]> git.cworth.org Git - obsolete/notmuch-old/commitdiff
Merge tag 'debian/0.15.1-1' into squeeze-backports
authorDavid Bremner <bremner@debian.org>
Wed, 6 Feb 2013 00:58:38 +0000 (20:58 -0400)
committerDavid Bremner <bremner@debian.org>
Wed, 6 Feb 2013 00:58:38 +0000 (20:58 -0400)
notmuch Debian 0.15.1-1 upload (same as 0.15.1)

191 files changed:
INSTALL
Makefile
Makefile.local
NEWS
bindings/python/docs/source/database.rst
bindings/python/docs/source/index.rst
bindings/python/docs/source/message.rst
bindings/python/docs/source/notmuch.rst [deleted file]
bindings/python/notmuch.py [deleted file]
bindings/python/notmuch/compat.py [new file with mode: 0644]
bindings/python/notmuch/database.py
bindings/python/notmuch/directory.py
bindings/python/notmuch/filenames.py
bindings/python/notmuch/globals.py
bindings/python/notmuch/message.py
bindings/python/notmuch/messages.py
bindings/python/notmuch/query.py
bindings/python/notmuch/tag.py
bindings/python/notmuch/thread.py
bindings/python/notmuch/threads.py
bindings/python/notmuch/version.py
bindings/ruby/defs.h
bindings/ruby/init.c
command-line-arguments.c
compat/README
compat/function-attributes.h [new file with mode: 0644]
configure
contrib/nmbug [deleted file]
contrib/nmbug/nmbug [new file with mode: 0755]
contrib/nmbug/nmbug-status [new file with mode: 0755]
contrib/nmbug/status-config.json [new file with mode: 0644]
contrib/notmuch-mutt/README
contrib/notmuch-mutt/notmuch-mutt
contrib/notmuch-mutt/notmuch-mutt.rc
contrib/notmuch-pick/README [new file with mode: 0644]
contrib/notmuch-pick/TODO [new file with mode: 0644]
contrib/notmuch-pick/notmuch-pick.el [new file with mode: 0644]
contrib/notmuch-pick/run-tests.sh [new file with mode: 0755]
contrib/notmuch-pick/test/emacs-pick [new file with mode: 0755]
contrib/notmuch-pick/test/emacs-pick-sync [new file with mode: 0755]
contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-show-window [new file with mode: 0644]
contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-single-thread [new file with mode: 0644]
contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-tag-inbox [new file with mode: 0644]
crypto.c [new file with mode: 0644]
debian/NEWS.Debian
debian/changelog
debian/compat
debian/control
debian/libnotmuch-dev.install
debian/libnotmuch3.install
debian/python-notmuch.install
debian/python3-notmuch.install [new file with mode: 0644]
debian/rules
devel/STYLE
devel/TODO
devel/release-checks.sh [new file with mode: 0755]
devel/schemata
devel/uncrustify.cfg
dump-restore-private.h [new file with mode: 0644]
emacs/notmuch-hello.el
emacs/notmuch-lib.el
emacs/notmuch-maildir-fcc.el
emacs/notmuch-message.el
emacs/notmuch-mua.el
emacs/notmuch-query.el
emacs/notmuch-show.el
emacs/notmuch-tag.el
emacs/notmuch-wash.el
emacs/notmuch.el
json.c [deleted file]
lib/Makefile.local
lib/database-private.h
lib/database.cc
lib/filenames.c
lib/index.cc
lib/message-file.c
lib/message.cc
lib/notmuch-private.h
lib/parse-time-vrp.cc [new file with mode: 0644]
lib/parse-time-vrp.h [new file with mode: 0644]
lib/tags.c
man/Makefile.local
man/man1/notmuch-config.1
man/man1/notmuch-count.1
man/man1/notmuch-dump.1
man/man1/notmuch-new.1
man/man1/notmuch-reply.1
man/man1/notmuch-restore.1
man/man1/notmuch-search.1
man/man1/notmuch-show.1
man/man1/notmuch-tag.1
man/man1/notmuch.1
man/man5/notmuch-hooks.5
man/man7/notmuch-search-terms.7
mime-node.c
notmuch-client.h
notmuch-config.c
notmuch-dump.c
notmuch-new.c
notmuch-reply.c
notmuch-restore.c
notmuch-search.c
notmuch-show.c
notmuch-tag.c
notmuch.c
parse-time-string/Makefile [new file with mode: 0644]
parse-time-string/Makefile.local [new file with mode: 0644]
parse-time-string/README [new file with mode: 0644]
parse-time-string/parse-time-string.c [new file with mode: 0644]
parse-time-string/parse-time-string.h [new file with mode: 0644]
performance-test/.gitignore [new file with mode: 0644]
performance-test/M00-new [new file with mode: 0755]
performance-test/M01-dump-restore [new file with mode: 0755]
performance-test/Makefile [new file with mode: 0644]
performance-test/Makefile.local [new file with mode: 0644]
performance-test/README [new file with mode: 0644]
performance-test/T00-new [new file with mode: 0755]
performance-test/T01-dump-restore [new file with mode: 0755]
performance-test/T02-tag [new file with mode: 0755]
performance-test/download/.gitignore [new file with mode: 0644]
performance-test/download/notmuch-email-corpus-0.3.tar.xz.asc [new file with mode: 0644]
performance-test/notmuch-time-test [new file with mode: 0755]
performance-test/perf-test-lib.sh [new file with mode: 0644]
performance-test/version.sh [new file with mode: 0644]
sprinter-json.c [new file with mode: 0644]
sprinter-sexp.c [new file with mode: 0644]
sprinter-text.c [new file with mode: 0644]
sprinter.h [new file with mode: 0644]
tag-util.c [new file with mode: 0644]
tag-util.h [new file with mode: 0644]
test/.gitignore
test/Makefile.local
test/README
test/arg-test.c
test/argument-parsing
test/atomicity
test/basic
test/config
test/count
test/crypto
test/database-test.c [new file with mode: 0644]
test/database-test.h [new file with mode: 0644]
test/dump-restore
test/emacs
test/emacs-address-cleaning
test/emacs-hello
test/emacs-large-search-buffer
test/emacs-show
test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-off [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-indent-thread-content-off [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-off [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-on [new file with mode: 0644]
test/emacs-subject-to-filename
test/emacs-test-functions
test/emacs.expected-output/notmuch-show-message-with-headers-hidden [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-message-with-headers-visible [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-with-all-messages-collapsed [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-with-all-messages-uncollapsed [new file with mode: 0644]
test/help-test
test/hex-escaping [new file with mode: 0755]
test/hex-xcode.c [new file with mode: 0644]
test/json
test/maildir-sync
test/missing-headers [new file with mode: 0755]
test/multipart
test/new
test/notmuch-test
test/parse-time-string [new file with mode: 0755]
test/parse-time.c [new file with mode: 0644]
test/random-corpus.c [new file with mode: 0644]
test/reply
test/search-date [new file with mode: 0755]
test/search-output
test/sexp [new file with mode: 0755]
test/smtp-dummy.c
test/tagging
test/test-lib-common.sh [new file with mode: 0644]
test/test-lib.el
test/test-lib.sh
test/text [new file with mode: 0755]
util/Makefile.local
util/error_util.c
util/error_util.h
util/hex-escape.c [new file with mode: 0644]
util/hex-escape.h [new file with mode: 0644]
util/string-util.c [new file with mode: 0644]
util/string-util.h [new file with mode: 0644]
util/talloc-extra.c [new file with mode: 0644]
util/talloc-extra.h [new file with mode: 0644]
version

diff --git a/INSTALL b/INSTALL
index bc98f1dec86ac2989aa1d67d402e725c33415233..fce935289c4a360dd5802d5b20590e2cbefbdc10 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -65,7 +65,7 @@ dependencies with a simple simple command line. For example:
 
   For Debian and similar:
 
-        sudo apt-get install libxapian-dev libgmime-2.4-dev libtalloc-dev
+        sudo apt-get install libxapian-dev libgmime-2.6-dev libtalloc-dev
 
   For Fedora and similar:
 
index e5e2e3a3ac67a9e515411c7e03d449db59d9a5c0..73a85546948fc71df0002adb1257c13aa248fc5b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,8 @@
 all:
 
 # List all subdirectories here. Each contains its own Makefile.local
-subdirs = compat completion emacs lib man util test
+subdirs := compat completion emacs lib man parse-time-string
+subdirs += performance-test util test
 
 # We make all targets depend on the Makefiles themselves.
 global_deps = Makefile Makefile.config Makefile.local \
index 53b4a0de8fce5dc51219c2c069dc355e4f6facae..c274f0736ce92c9824451d0de60cb27ee67af3e6 100644 (file)
@@ -187,7 +187,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-version-python verify-version-manpage
+verify-no-dirty-code: release-checks
 ifeq ($(IS_GIT),yes)
        @printf "Checking that source tree is clean..."
 ifneq ($(shell git ls-files -m),)
@@ -204,29 +204,9 @@ else
 endif
 endif
 
-.PHONY: verify-version-debian
-verify-version-debian: verify-version-components
-       @echo -n "Checking that Debian package version is $(VERSION)-1..."
-       @[ "$(VERSION)-1" = $$(sed '1{ s/).*//; s/.*(//; q; }' debian/changelog) ] || \
-               (echo "No." && \
-                echo "Please edit version and debian/changelog to have consistent versions." && false)
-       @echo "Good."
-
-.PHONY: verify-version-python
-verify-version-python: verify-version-components
-       @echo -n "Checking that python bindings version is $(VERSION)..."
-       @[ "$(VERSION)" = $$(python -c "execfile('$(PV_FILE)'); print __VERSION__") ] || \
-               (echo "No." && \
-                echo "Please edit version and $(PV_FILE) to have consistent versions." && false)
-       @echo "Good."
-
-.PHONY: verify-version-components
-verify-version-components:
-       @echo -n "Checking that $(VERSION) consists only of digits and periods..."
-       @echo $(VERSION) | grep -q -x '^[0-9.]*$$' || \
-               (echo "No." && \
-                echo "Please follow the instructions in RELEASING to choose a version" && false)
-       @echo "Good."
+.PHONY: release-checks
+release-checks:
+       devel/release-checks.sh
 
 .PHONY: verify-newer
 verify-newer:
@@ -265,12 +245,11 @@ quiet ?= $($(shell echo $1 | sed -e s'/ .*//'))
 
 .PHONY : clean
 clean:
-       rm -f $(CLEAN); rm -rf .deps
+       rm -rf $(CLEAN); rm -rf .deps
 
-# We don't (yet) have any distributed files not in the upstream repository.
-# So distclean is currently identical to clean.
 .PHONY: distclean
 distclean: clean
+       rm -rf $(DISTCLEAN)
 
 notmuch_client_srcs =          \
        command-line-arguments.c\
@@ -290,13 +269,17 @@ notmuch_client_srcs =             \
        notmuch-show.c          \
        notmuch-tag.c           \
        notmuch-time.c          \
+       sprinter-json.c         \
+       sprinter-sexp.c         \
+       sprinter-text.c         \
        query-string.c          \
        mime-node.c             \
-       json.c
+       crypto.c                \
+       tag-util.c
 
 notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
 
-notmuch: $(notmuch_client_modules) lib/libnotmuch.a util/libutil.a
+notmuch: $(notmuch_client_modules) lib/libnotmuch.a util/libutil.a parse-time-string/libparse-time-string.a
        $(call quiet,CXX $(CFLAGS)) $^ $(FINAL_LIBNOTMUCH_LDFLAGS) -o $@
 
 notmuch-shared: $(notmuch_client_modules) lib/$(LINKER_NAME)
@@ -336,6 +319,8 @@ install-desktop:
 SRCS  := $(SRCS) $(notmuch_client_srcs)
 CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) notmuch.elc
 
+DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config
+
 DEPS := $(SRCS:%.c=.deps/%.d)
 DEPS := $(DEPS:%.cc=.deps/%.d)
 -include $(DEPS)
diff --git a/NEWS b/NEWS
index fb55efb7d30d735dfcab260c3c45d5ac8fd864f6..97f23058150d1d90daab39d9acbee53df626c4cf 100644 (file)
--- a/NEWS
+++ b/NEWS
+Notmuch 0.15.1 (2013-01-24)
+=========================
+
+Internal test framework changes
+-------------------------------
+
+Set a default value for TERM when running tests. This fixes certain
+build failures in non-interactive environments.
+
+Notmuch 0.15 (2013-01-18)
+=========================
+
+General
+-------
+
+Date range search support
+
+  The `date:` prefix can now be used in queries to restrict the results
+  to only messages within a particular time range (based on the Date:
+  header) with a range syntax of `date:<since>..<until>`. Notmuch
+  supports a wide variety of expressions in `<since>` and
+  `<until>`. Please refer to the `notmuch-search-terms(7)` manual page
+  for details.
+
+Empty tag names and tags beginning with "-" are deprecated
+
+  Such tags have been a frequent source of confusion and cause
+  (sometimes unresolvable) conflicts with other syntax.  notmuch tag
+  no longer allows such tags to be added to messages.  Removing such
+  tags continues to be supported to allow cleanup of existing tags,
+  but may be removed in a future release.
+
+Command-Line Interface
+----------------------
+
+`notmuch new` no longer chokes on mboxes
+
+  `notmuch new` now rejects mbox files containing more than one
+  message, rather than treating the file as one giant message.
+
+Support for single message mboxes is deprecated
+
+  For historical reasons, `notmuch new` will index mbox files
+  containing a single message; however, this behavior is now
+  officially deprecated.
+
+Fixed `notmuch new` to skip ignored broken symlinks
+
+  `notmuch new` now correctly skips symlinks if they are in the
+  ignored files list.  Previously, it would abort when encountering
+  broken symlink, even if it was ignored.
+
+New dump/restore format and tagging interface
+
+  There is a new `batch-tag` format for dump and restore that is more
+  robust, particularly with respect to tags and message-ids containing
+  whitespace.
+
+  `notmuch tag` now supports the ability to read tag operations and
+  queries from an input stream, in a format compatible with the new
+  dump/restore format.
+
+Bcc and Reply-To headers are now available in notmuch show json output
+
+  The `notmuch show --format=json` now includes "Bcc" and "Reply-To" headers.
+  For example notmuch Emacs client can now have these headers visible
+  when the headers are added to the `notmuch-message-headers` variable.
+
+CLI callers can now request a specific output format version
+
+  `notmuch` subcommands that support structured output now support a
+  `--format-version` argument for requesting a specific version of the
+  structured output, enabling better compatibility and error handling.
+
+`notmuch search` has gained a null character separated text output format
+
+  The new --format=text0 output format for `notmuch search` prints
+  output separated by null characters rather than newline
+  characters. This is similar to the find(1) -print0 option, and works
+  together with the xargs(1) -0 option.
+
+Emacs Interface
+---------------
+
+Removal of the deprecated `notmuch-folders` variable
+
+  `notmuch-folders` has been deprecated since the introduction of saved
+  searches and the notmuch hello view in notmuch 0.3. `notmuch-folders`
+  has now been removed. Any remaining users should migrate to
+  `notmuch-saved-searches`.
+
+Visibility of MIME parts can be toggled
+
+  Each part of a multi-part MIME email can now be shown or hidden
+  using the button at the top of each part (by pressing RET on it or
+  by clicking).  For emails with multiple alternative formats (e.g.,
+  plain text and HTML), only the preferred format is shown initially,
+  but other formats can be shown using their part buttons.  To control
+  the behavior of this, see
+  `notmuch-multipart/alternative-discouraged` and
+  `notmuch-show-all-multipart/alternative-parts`.
+
+  Note notmuch-show-print-message (bound to '#' by default) will print
+  all parts of multipart/alternative message regardless of whether
+  they are currently hidden or shown in the buffer.
+
+Emacs now buttonizes mid: links
+
+  mid: links are a standardized way to link to messages by message ID
+  (see RFC 2392).  Emacs now hyperlinks mid: links to the appropriate
+  notmuch search.
+
+Handle errors from bodypart insertions
+
+  If displaying the text of a message in show mode causes an error (in
+  the `notmuch-show-insert-part-*` functions), notmuch no longer cuts
+  off thread display at the offending message.  The error is now
+  simply displayed in place of the message.
+
+Emacs now detects version mismatches with the notmuch CLI
+
+  Emacs now detects and reports when the Emacs interface version and
+  the notmuch CLI version are incompatible.
+
+Improved text/calendar content handling
+
+  Carriage returns in embedded text/calendar content caused insertion
+  of the calendar content fail. Now CRs are removed before calling icalendar
+  to extract icalendar data. In case icalendar extraction fails an error
+  is thrown for the bodypart insertion function to deal with.
+
+Disabled coding conversions when reading in `with-current-notmuch-show-message`
+
+  Depending on the user's locale, saving attachments containing 8-bit
+  data may have performed an unintentional encoding conversion,
+  corrupting the saved attachment.  This has been fixed by making
+  `with-current-notmuch-show-message` disable coding conversion.
+
+Fixed errors with HTML email containing images in Emacs 24
+
+  Emacs 24 ships with a new HTML renderer that produces better output,
+  but is slightly buggy.  We work around a bug that caused it to fail
+  for HTML email containing images.
+
+Fixed handling of tags with unusual characters in them
+
+  Emacs now handles tags containing spaces, quotes, and parenthesis.
+
+Fixed buttonization of id: links without quote characters
+
+  Emacs now correctly buttonizes id: links where the message ID is not
+  quoted.
+
+`notmuch-hello` refresh point placement improvements
+
+  Refreshing the `notmuch-hello` buffer does a better job of keeping
+  the point where it was.
+
+Automatic tag changes are now unified and customizable
+
+  All the automatic tag changes that the Emacs interface makes when
+  reading, archiving, or replying to messages, can now be
+  customized. Any number of tag additions and removals is supported
+  through the `notmuch-show-mark-read`, `notmuch-archive-tags`, and
+  `notmuch-message-replied-tags` customization variables.
+
+Support for stashing the thread id in show view
+
+  Invoking `notmuch-show-stash-message-id` with a prefix argument
+  stashes the (local and database specific) thread id of the current
+  thread instead of the message id.
+
+New add-on tool: notmuch-pick
+-----------------------------
+
+The new contrib/ tool `notmuch-pick` is an experimental threaded message
+view for the emacs interface. Each message is one line in the results
+and the thread structure is shown using UTF-8 box drawing characters
+(similar to Mutt's threaded view). It comes between search and show in
+terms of amount of output and can be useful for viewing both single
+threads and multiple threads. See the notmuch-pick README file for
+further details and installation.
+
+Portability
+-----------
+
+notmuch now builds on OpenBSD.
+
+Internal test framework changes
+-------------------------------
+
+The emacsclient binary is now user-configurable
+
+  The test framework now accepts TEST_EMACSCLIENT in addition to
+  TEST_EMACS for configuring the emacsclient to use.  This is
+  necessary to avoid using an old emacsclient with a new emacs, which
+  can result in buggy behavior.
+
+Notmuch 0.14 (2012-08-20)
+=========================
+
+General bug fixes
+-----------------
+
+Maildir tag synchronization
+
+  Maildir flag-to-tag synchronization now applies only to messages in
+  maildir-like directory structures.  Previously, it applied to any
+  message that had a maildir "info" part, which meant it could
+  incorrectly synchronize tags for non-maildir messages, while at the
+  same time failing to synchronize tags for newly received maildir
+  messages (typically causing new messages to not receive the "unread"
+  tag).
+
+Command-Line Interface
+----------------------
+
+  The deprecated positional output file argument to `notmuch dump` has
+  been replaced with an `--output` option. The input file positional
+  argument to `notmuch restore` has been replaced with an `--input`
+  option for consistency with dump.  These changes simplify the syntax
+  of dump/restore options and make them more consistent with other
+  notmuch commands.
+
+Emacs Interface
+---------------
+
+Search results now get re-colored when tags are updated
+
+The formatting of tags in search results can now be customized
+
+  Previously, attempting to change the format of tags in
+  `notmuch-search-result-format` would usually break tagging from
+  search-mode.  We no longer make assumptions about the format.
+
+Experimental support for multi-line search result formats
+
+  It is now possible to embed newlines in
+  `notmuch-search-result-format` to make individual search results
+  span multiple lines.
+
+Next/previous in search and show now move by boundaries
+
+  All "next" and "previous" commands in the search and show modes now
+  move to the next/previous result or message boundary.  This doesn't
+  change the behavior of "next", but "previous" commands will first
+  move to the beginning of the current result or message if point is
+  inside the result or message.
+
+Search now uses the JSON format internally
+
+  This should address problems with unusual characters in authors and
+  subject lines that could confuse the old text-based search parser.
+
+The date shown in search results is no longer padded before applying
+user-specified formatting
+
+  Previously, the date in the search results was padded to fixed width
+  before being formatted with `notmuch-search-result-format`.  It is
+  no longer padded.  The default format has been updated, but if
+  you've customized this variable, you may have to change your date
+  format from `"%s "` to `"%12s "`.
+
+The thread-id for the `target-thread` argument for `notmuch-search` should
+now be supplied without the "thread:" prefix.
+
 Notmuch 0.13.2 (2012-06-02)
 ===========================
 
 Bug-fix release
 ---------------
 
-Update contrib/notmuch-deliver for API changes in 0.13. This fixes a
+Update `contrib/notmuch-deliver` for API changes in 0.13. This fixes a
 compilation error for this contrib package.
 
 Notmuch 0.13.1 (2012-05-29)
@@ -30,7 +296,7 @@ databases
   and simply return a `NULL` object if the directory does not exist,
   as documented.
 
-Fix compilation of ruby bindings.
+Fix compilation of ruby bindings
 
   Revert to dynamic linking, since the statically linked bindings did
   not work well.
index 2464bffff3273c805928555afcf51b3c5203c508..5f1cdc14b3f6dcbf107d4b708bf7a9228fdbdd7c 100644 (file)
@@ -46,5 +46,3 @@
 
      MODE.READ_WRITE
        Open the database in read-write mode
-
-   .. autoattribute:: db_p
index 9ad5fa9759fc3e3db4e738fc8854dd07144780e3..1cece5f70f25d45c9e4463a25c1145666c711a78 100644 (file)
@@ -28,7 +28,6 @@ functionality, returning :class:`Threads`, :class:`Messages` and
    threads
    thread
    filesystem
-   notmuch
 
 Indices and tables
 ==================
index 2ae280e30d9a44dd7f0e5adce543d78d5329e5f8..1a6cc3d52240a0881af9a429c584694339cbd287 100644 (file)
@@ -47,8 +47,4 @@
 
    .. automethod:: thaw
 
-   .. automethod:: format_message_as_json
-
-   .. automethod:: format_message_as_text
-
    .. automethod:: __str__
diff --git a/bindings/python/docs/source/notmuch.rst b/bindings/python/docs/source/notmuch.rst
deleted file mode 100644 (file)
index bf68f33..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-The notmuch 'binary'
-====================
-
-The cnotmuch module provides *notmuch*, a python reimplementation of the standard notmuch binary for two purposes: first, to allow running the standard notmuch testsuite over the cnotmuch bindings (for correctness and performance testing) and second, to give some examples as to how to use cnotmuch. 'Notmuch' provides a command line interface to your mail database.
-
-A standard install via `easy_install cnotmuch` will not install the notmuch binary, however it is available in the `cnotmuch source code repository <http://bitbucket.org/spaetz/cnotmuch/src/>`_.
-
-
-It is invoked with the following pattern: `notmuch <command> [args...]`.
-
-Where <command> and [args...] are as follows:
-
-  **setup**    Interactively setup notmuch for first use.
-                This has not yet been implemented, and will probably not be
-               implemented unless someone puts in the effort.
-
-  **new**      [--verbose]
-               Find and import new messages to the notmuch database.
-
-               This has not been implemented yet. We cheat by calling
-               the regular "notmuch" binary (which must be in your path
-               somewhere).
-
-  **search** [options...] <search-terms> [...]  Search for messages matching the given search terms.
-
-               This has been implemented but for the `--format` and
-               `--sort` options.
-
-  **show**     <search-terms> [...]
-               Show all messages matching the search terms.
-
-               This has been partially implemented, we show a stub for each
-               found message, but do not output the full message body yet.
-
-  **count**    <search-terms> [...]
-               Count messages matching the search terms.
-
-               This has been fully implemented.
-
-  **reply**    [options...] <search-terms> [...]
-               Construct a reply template for a set of messages.
-
-               This has not been implemented yet.
-
-  **tag**      +<tag>|-<tag> [...] [--] <search-terms> [...]
-               Add/remove tags for all messages matching the search terms.
-
-               This has been fully implemented.
-
-  **dump**     [<filename>]
-               Create a plain-text dump of the tags for each message.
-
-               This has been fully implemented.
-  **restore**  <filename>
-               Restore the tags from the given dump file (see 'dump').
-
-               This has been fully implemented.
-
-  **search-tags**      [<search-terms> [...] ]
-               List all tags found in the database or matching messages.
-
-               This has been fully implemented.
-
-  **help**     [<command>]
-               This message, or more detailed help for the named command.
-
-               The 'help' page has been implemented, help for single
-               commands are missing though. Patches are welcome.
diff --git a/bindings/python/notmuch.py b/bindings/python/notmuch.py
deleted file mode 100755 (executable)
index 3ff53ec..0000000
+++ /dev/null
@@ -1,651 +0,0 @@
-#!/usr/bin/env python
-"""This is a notmuch implementation in python.
-It's goal is to allow running the test suite on the cnotmuch python bindings.
-
-This "binary" honors the NOTMUCH_CONFIG environmen variable for reading a user's
-notmuch configuration (e.g. the database path).
-
-   (c) 2010 by Sebastian Spaeth <Sebastian@SSpaeth.de>
-               Jesse Rosenthal <jrosenthal@jhu.edu>
-   This code is licensed under the GNU GPL v3+.
-"""
-import sys
-import os
-
-import re
-import stat
-import email
-
-from notmuch import Database, Query, NotmuchError, STATUS
-try:
-    # python3.x
-    from configparser import SafeConfigParser
-except ImportError:
-    # python2.x
-    from ConfigParser import SafeConfigParser
-from cStringIO import StringIO
-
-PREFIX = re.compile('(\w+):(.*$)')
-
-HELPTEXT = """The notmuch mail system.
-Usage: notmuch <command> [args...]
-
-Where <command> and [args...] are as follows:
-       setup   Interactively setup notmuch for first use.
-       new     [--verbose]
-               Find and import new messages to the notmuch database.
-       search  [options...] <search-terms> [...]
-               Search for messages matching the given search terms.
-       show    <search-terms> [...]
-               Show all messages matching the search terms.
-       count   <search-terms> [...]
-               Count messages matching the search terms.
-       reply   [options...] <search-terms> [...]
-               Construct a reply template for a set of messages.
-       tag     +<tag>|-<tag> [...] [--] <search-terms> [...]
-               Add/remove tags for all messages matching the search terms.
-       dump    [<filename>]
-               Create a plain-text dump of the tags for each message.
-       restore <filename>
-               Restore the tags from the given dump file (see 'dump').
-       search-tags     [<search-terms> [...] ]
-               List all tags found in the database or matching messages.
-       help    [<command>]
-               This message, or more detailed help for the named command.
-
-Use "notmuch help <command>" for more details on each command.
-And "notmuch help search-terms" for the common search-terms syntax.
-"""
-
-USAGE = """Notmuch is configured and appears to have a database. Excellent!
-
-At this point you can start exploring the functionality of notmuch by
-using commands such as:
-       notmuch search tag:inbox
-       notmuch search to:"%(fullname)s"
-       notmuch search from:"%(mailaddress)s"
-       notmuch search subject:"my favorite things"
-
-See "notmuch help search" for more details.
-
-You can also use "notmuch show" with any of the thread IDs resulting
-from a search. Finally, you may want to explore using a more sophisticated
-interface to notmuch such as the emacs interface implemented in notmuch.el
-or any other interface described at http://notmuchmail.org
-
-And don't forget to run "notmuch new" whenever new mail arrives.
-
-Have fun, and may your inbox never have much mail.
-"""
-
-#-------------------------------------------------------------------------
-def quote_query_line(argv):
-    # mangle arguments wrapping terms with spaces in quotes
-    for (num, item) in enumerate(argv):
-        if item.find(' ') >= 0:
-               # if we use prefix:termWithSpaces, put quotes around term
-               match = PREFIX.match(item)
-                if match:
-                       argv[num] = '%s:"%s"' %(match.group(1), match.group(2))
-               else:
-                       argv[num] = '"%s"' % item
-    return ' '.join(argv)
-
-#-------------------------------------------------------------------------
-
-
-class Notmuch(object):
-
-    def __init__(self, configpath="~/.notmuch-config)"):
-        self._config = None
-        self._configpath = os.getenv('NOTMUCH_CONFIG',
-            os.path.expanduser(configpath))
-
-    def cmd_usage(self):
-        """Print the usage text and exits"""
-        data={}
-        names = self.get_user_email_addresses()
-        data['fullname'] = names[0] if names[0] else 'My Name'
-        data['mailaddress'] = names[1] if names[1] else 'My@email.address'
-        print USAGE % data
-
-    def cmd_new(self):
-        """Run 'notmuch new'"""
-        #get the database directory
-        db = Database(mode=Database.MODE.READ_WRITE)
-        path = db.get_path()
-        print self._add_new_files_recursively(path, db)
-
-    def cmd_help(self, subcmd=None):
-        """Print help text for 'notmuch help'"""
-        if len(subcmd) > 1:
-            print "Help for specific commands not implemented"
-            return
-        print HELPTEXT
-
-    def _get_user_notmuch_config(self):
-        """Returns the ConfigParser of the user's notmuch-config"""
-       # return the cached config parser if we read it already
-       if self._config:
-            return self._config
-
-       config = SafeConfigParser()
-       config.read(self._configpath)
-       self._config = config
-       return config
-
-    def _add_new_files_recursively(self, path, db):
-        """:returns: (added, moved, removed)"""
-        print "Enter add new files with path %s" % path
-
-        try:
-            #get the Directory() object for this path
-            db_dir = db.get_directory(path)
-            added = moved = removed = 0
-        except NotmuchError:
-            # Occurs if we have wrong absolute paths in the db, for example
-            return (0,0,0)
-
-
-        # for folder in subdirs:
-
-        # TODO, retrieve dir mtime here and store it later
-        # as long as Filenames() does not allow multiple iteration, we need to
-        # use this kludgy way to get a sorted list of filenames
-        # db_files is a list of subdirectories and filenames in this folder
-        db_files = set()
-        db_folders = set()
-        for subdir in db_dir.get_child_directories():
-            db_folders.add(subdir)
-# file is a keyword (remove this ;))
-        for mail in db_dir.get_child_files():
-            db_files.add(mail)
-
-        fs_files = set(os.listdir(db_dir.path))
-
-        # list of files (and folders) on the fs, but not the db
-        for fs_file in ((fs_files - db_files) - db_folders):
-            absfile = os.path.normpath(os.path.join(db_dir.path, fs_file))
-            statinfo = os.stat(absfile)
-
-            if stat.S_ISDIR(statinfo.st_mode):
-                # This is a directory
-                if fs_file in ['.notmuch','tmp','.']:
-                    continue
-               print "%s %s" % (fs_file, db_folders)
-                print "Directory not in db yet. Descending into %s" % absfile
-                new = self._add_new_files_recursively(absfile, db)
-                added += new[0]
-                moved += new[1]
-                removed += new[2]
-
-            elif stat.S_ISLNK(statinfo.st_mode):
-                print ("%s is a symbolic link (%d). FIXME!!!" %
-                       (absfile, statinfo.st_mode))
-                exit(1)
-
-            else:
-                # This is a regular file, not in the db yet. Add it.
-                print "This file needs to be added %s" % (absfile)
-                (msg, status) = db.add_message(absfile)
-                # We increases 'added', even on dupe messages. If it is a moved
-                # message, we will deduct it later and increase 'moved' instead
-                added += 1
-
-                if status == STATUS.DUPLICATE_MESSAGE_ID:
-                    print "Added msg was in the db"
-                else:
-                    print "New message."
-
-        # Finally a list of files (not folders) in the database,
-        # but not the filesystem
-        for db_file in (db_files - fs_files):
-            absfile = os.path.normpath(os.path.join(db_dir.path, db_file))
-
-            # remove a mail message from the db
-            print ("%s is not on the fs anymore. Delete" % absfile)
-            status = db.remove_message(absfile)
-
-            if status == STATUS.SUCCESS:
-                # we just deleted the last reference, so this was a remove
-                removed += 1
-                sys.stderr.write("SUCCESS %d %s %s.\n" %
-                        (status, STATUS.status2str(status), absfile))
-            elif status == STATUS.DUPLICATE_MESSAGE_ID:
-                # The filename exists already somewhere else, so this is a move
-                moved += 1
-                added -= 1
-                sys.stderr.write("DUPE %d %s %s.\n" %
-                        (status, STATUS.status2str(status), absfile))
-            else:
-                # This should not occur
-                sys.stderr.write("This should not occur %d %s %s.\n" %
-                        (status, STATUS.status2str(status), absfile))
-
-        # list of folders in the filesystem. Just descend into dirs
-        for fs_file in fs_files:
-            absfile = os.path.normpath(os.path.join(db_dir.path, fs_file))
-            if os.path.isdir(absfile):
-                # This is a directory. Remove it from the db_folder list. 
-                # All remaining db_folders at the end will be not present
-                # on the file system.
-                db_folders.remove(fs_file)
-                if fs_file in ['.notmuch','tmp','.']:
-                    continue
-                new = self._add_new_files_recursively(absfile, db)
-                added += new[0]
-                moved += new[0]
-                removed += new[0]
-
-        # we are not interested in anything but directories here
-        #TODO: All remaining elements of db_folders are not in the filesystem
-        #delete those
-
-        return added, moved, removed
-        #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 
-        #  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 
-        #  directory and files are newer than the stored timestamp.
-
-    def get_user_email_addresses(self):
-        """ Reads a user's notmuch config and returns his email addresses as
-       list (name, primary_address, other_address1,...)"""
-
-       #read the config file
-       config = self._get_user_notmuch_config()
-
-        conf = {'name': '', 'primary_email': ''}
-        for entry in conf:
-            if config.has_option('user', entry):
-                conf[entry] = config.get('user', entry)
-
-       if config.has_option('user','other_email'):
-            other = config.get('user','other_email')
-            other = [mail.strip() for mail in other.split(';') if mail]
-        else:
-            other = []
-        # for being compatible. It would be nicer to return a dict.
-       return conf.keys() + other
-
-    def quote_msg_body(self, oldbody ,date, from_address):
-        """Transform a mail body into a quoted text,
-        starting with On foo, bar wrote:
-
-        :param body: a str with a mail body
-        :returns: The new payload of the email.message()
-        """
-
-        # we get handed a string, wrap it in a file-like object
-        oldbody = StringIO(oldbody)
-        newbody = StringIO()
-
-        newbody.write("On %s, %s wrote:\n" % (date, from_address))
-
-        for line in oldbody:
-            newbody.write("> " + line)
-
-        return newbody.getvalue()
-
-    def format_reply(self, msgs):
-        """Gets handed Messages() and displays the reply to them
-
-        This is pretty ugly and hacky. It tries to mimic the "real"
-        notmuch output as much as it can to pass the test suite. It
-        could deserve a healthy bit of love.  It is also buggy because
-        it returns after the first message it has handled."""
-
-        for msg in msgs:
-            f = open(msg.get_filename(), "r")
-            reply = email.message_from_file(f)
-
-            # handle the easy non-multipart case:
-            if not reply.is_multipart():
-                reply.set_payload(self.quote_msg_body(reply.get_payload(),
-                    reply['date'], reply['from']))
-            else:
-                # handle the tricky multipart case
-                deleted = ""
-                """A string describing which nontext attachements
-                that have been deleted"""
-                delpayloads = []
-                """A list of payload indices to be deleted"""
-                payloads = reply.get_payload()
-
-                for (num, part) in enumerate(payloads):
-                    mime_main = part.get_content_maintype()
-                    if mime_main not in ['multipart', 'message', 'text']:
-                        deleted += "Non-text part: %s\n" % (part.get_content_type())
-                        payloads[num].set_payload("Non-text part: %s" %
-                                (part.get_content_type()))
-                        payloads[num].set_type('text/plain')
-                        delpayloads.append(num)
-                    elif mime_main == 'text':
-                        payloads[num].set_payload(self.quote_msg_body(
-                            payloads[num].get_payload(),
-                            reply['date'], reply['from']))
-                    else:
-                        # TODO handle deeply nested multipart messages
-                        sys.stderr.write ("FIXME: Ignoring multipart part. Handle me\n")
-                # Delete those payloads that we don't need anymore
-                for item in reversed(sorted(delpayloads)):
-                    del payloads[item]
-
-        # Back to single- and multipart handling
-        my_addresses = self.get_user_email_addresses()
-        used_address = None
-        # filter our email addresses from all to: cc: and bcc: fields
-        # if we find one of "my" addresses being used, 
-        # it is stored in used_address
-        for header in ['To', 'CC', 'Bcc']:
-            if not header in reply:
-                #only handle fields that exist
-                continue
-            addresses = email.utils.getaddresses(reply.get_all(header, []))
-            purged_addr = []
-            for (name, mail) in addresses:
-                if mail in my_addresses[1:]:
-                    used_address = email.utils.formataddr(
-                            (my_addresses[0], mail))
-                else:
-                    purged_addr.append(email.utils.formataddr((name, mail)))
-
-            if purged_addr:
-                reply.replace_header(header, ", ".join(purged_addr))
-            else:
-                # we deleted all addresses, delete the header
-                del reply[header]
-
-        # Use our primary email address to the From
-        # (save original from line, we still need it)
-        new_to = reply['From']
-        if used_address:
-            reply['From'] = used_address
-        else:
-            email.utils.formataddr((my_addresses[0], my_addresses[1]))
-
-        reply['Subject'] = 'Re: ' + reply['Subject']
-
-        # Calculate our new To: field
-        # add all remaining original 'To' addresses
-        if 'To' in reply:
-            new_to += ", " + reply['To']
-        reply.add_header('To', new_to)
-
-        # Add our primary email address to the BCC
-        new_bcc = my_addresses[1]
-        if 'Bcc' in reply:
-            new_bcc += ', '  + reply['Bcc']
-        reply['Bcc'] = new_bcc
-
-        # Set replies 'In-Reply-To' header to original's Message-ID
-        if 'Message-ID' in reply:
-            reply['In-Reply-To'] = reply['Message-ID']
-
-        #Add original's Message-ID to replies 'References' header.
-        if 'References' in reply:
-            reply['References'] =  ' '.join([reply['References'], reply['Message-ID']])
-        else:
-            reply['References'] = reply['Message-ID']
-
-        # Delete the original Message-ID.
-        del(reply['Message-ID'])
-
-        # filter all existing headers but a few and delete them from 'reply'
-        delheaders = filter(lambda x: x not in ['From', 'To', 'Subject', 'CC',
-                                                'Bcc', 'In-Reply-To',
-                                                'References', 'Content-Type'],
-                            reply.keys())
-        map(reply.__delitem__, delheaders)
-
-        # TODO: OUCH, we return after the first msg we have handled rather than
-        # handle all of them
-        # return resulting message without Unixfrom
-        return reply.as_string(False)
-
-
-def main():
-    # Handle command line options
-    #------------------------------------
-    # No option given, print USAGE and exit
-    if len(sys.argv) == 1:
-        Notmuch().cmd_usage()
-    #------------------------------------
-    elif sys.argv[1] == 'setup':
-       """Interactively setup notmuch for first use."""
-       exit("Not implemented.")
-    #-------------------------------------
-    elif sys.argv[1] == 'new':
-        """Check for new and removed messages."""
-        Notmuch().cmd_new()
-    #-------------------------------------
-    elif sys.argv[1] == 'help':
-        """Print the help text"""
-        Notmuch().cmd_help(sys.argv[1:])
-    #-------------------------------------
-    elif sys.argv[1] == 'part':
-        part()
-    #-------------------------------------
-    elif sys.argv[1] == 'search':
-        search()
-    #-------------------------------------
-    elif sys.argv[1] == 'show':
-        show()
-    #-------------------------------------
-    elif sys.argv[1] == 'reply':
-        db = Database()
-        if len(sys.argv) == 2:
-            # no search term. abort
-            exit("Error: notmuch reply requires at least one search term.")
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[2:])
-        msgs = Query(db, querystr).search_messages()
-        print Notmuch().format_reply(msgs)
-    #-------------------------------------
-    elif sys.argv[1] == 'count':
-        if len(sys.argv) == 2:
-            # no further search term, count all
-            querystr = ''
-        else:
-            # mangle arguments wrapping terms with spaces in quotes
-            querystr = quote_query_line(sys.argv[2:])
-       print Database().create_query(querystr).count_messages()
-    #-------------------------------------
-    elif sys.argv[1] == 'tag':
-        # build lists of tags to be added and removed
-        add = []
-        remove = []
-        while not sys.argv[2] == '--' and \
-                (sys.argv[2].startswith('+') or sys.argv[2].startswith('-')):
-                    if sys.argv[2].startswith('+'):
-                        # append to add list without initial +
-                        add.append(sys.argv.pop(2)[1:])
-                    else:
-                        # append to remove list without initial -
-                        remove.append(sys.argv.pop(2)[1:])
-        # skip eventual '--'
-        if sys.argv[2] == '--': sys.argv.pop(2)
-        # the rest is search terms
-        querystr = quote_query_line(sys.argv[2:])
-        db = Database(mode=Database.MODE.READ_WRITE)
-        msgs  = Query(db, querystr).search_messages()
-        for msg in msgs:
-            # actually add and remove all tags
-            map(msg.add_tag, add)
-            map(msg.remove_tag, remove)
-    #-------------------------------------
-    elif sys.argv[1] == 'search-tags':
-        if len(sys.argv) == 2:
-            # no further search term
-            print "\n".join(Database().get_all_tags())
-        else:
-            # mangle arguments wrapping terms with spaces in quotes
-            querystr = quote_query_line(sys.argv[2:])
-            db = Database()
-            msgs  = Query(db, querystr).search_messages()
-            print "\n".join([t for t in msgs.collect_tags()])
-    #-------------------------------------
-    elif sys.argv[1] == 'dump':
-        if len(sys.argv) == 2:
-            f = sys.stdout
-        else:
-            f = open(sys.argv[2], "w")
-        db = Database()
-        query = Query(db, '')
-        query.set_sort(Query.SORT.MESSAGE_ID)
-        msgs = query.search_messages()
-        for msg in msgs:
-            f.write("%s (%s)\n" % (msg.get_message_id(), msg.get_tags()))
-    #-------------------------------------
-    elif sys.argv[1] == 'restore':
-        if len(sys.argv) == 2:
-            print("No filename given. Reading dump from stdin.")
-            f = sys.stdin
-        else:
-            f = open(sys.argv[2], "r")
-
-        # split the msg id and the tags
-        MSGID_TAGS = re.compile("(\S+)\s\((.*)\)$")
-        db = Database(mode=Database.MODE.READ_WRITE)
-
-        #read each line of the dump file
-        for line in f:
-            msgs = MSGID_TAGS.match(line)
-            if not msgs:
-                sys.stderr.write("Warning: Ignoring invalid input line: %s" %
-                        line)
-                continue
-            # split line in components and fetch message
-            msg_id = msgs.group(1)
-            new_tags = set(msgs.group(2).split())
-            msg = db.find_message(msg_id)
-
-            if msg == None:
-                sys.stderr.write(
-                        "Warning: Cannot apply tags to missing message: %s\n" % msg_id)
-                continue
-
-            # do nothing if the old set of tags is the same as the new one
-            old_tags = set(msg.get_tags())
-            if old_tags == new_tags: continue
-
-            # set the new tags
-            msg.freeze()
-            # only remove tags if the new ones are not a superset anyway
-            if not (new_tags > old_tags): msg.remove_all_tags()
-            for tag in new_tags: msg.add_tag(tag)
-            msg.thaw()
-    #-------------------------------------
-    else:
-        # unknown command
-        exit("Error: Unknown command '%s' (see \"notmuch help\")" % sys.argv[1])
-
-def part():
-    db = Database()
-    query_string = ''
-    part_num = 0
-    first_search_term = 0
-    for (num, arg) in enumerate(sys.argv[1:]):
-        if arg.startswith('--part='):
-            part_num_str = arg.split("=")[1]
-            try:
-                part_num = int(part_num_str)
-            except ValueError:
-                # just emulating behavior
-                exit(1)
-        elif not arg.startswith('--'):
-            # save the position of the first sys.argv
-            # that is a search term
-            first_search_term = num + 1
-    if first_search_term:
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[first_search_term:])
-    qry = Query(db,querystr)
-    msgs = [msg for msg in qry.search_messages()]
-
-    if not msgs:
-        sys.exit(1)
-    elif len(msgs) > 1:
-        raise Exception("search term did not match precisely one message")
-    else:
-        msg = msgs[0]
-        print msg.get_part(part_num)
-
-def search():
-    db = Database()
-    query_string = ''
-    sort_order = "newest-first"
-    first_search_term = 0
-    for (num, arg) in enumerate(sys.argv[1:]):
-        if arg.startswith('--sort='):
-            sort_order=arg.split("=")[1]
-            if not sort_order in ("oldest-first", "newest-first"):
-                raise Exception("unknown sort order")
-        elif not arg.startswith('--'):
-            # save the position of the first sys.argv that is a search term
-            first_search_term = num + 1
-
-    if first_search_term:
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[first_search_term:])
-
-    qry = Query(db, querystr)
-    if sort_order == "oldest-first":
-        qry.set_sort(Query.SORT.OLDEST_FIRST)
-    else:
-        qry.set_sort(Query.SORT.NEWEST_FIRST)
-        threads = qry.search_threads()
-
-    for thread in threads:
-        print thread
-
-def show():
-    entire_thread = False
-    db = Database()
-    out_format = "text"
-    querystr = ''
-    first_search_term = None
-
-    # ugly homegrown option parsing
-    # TODO: use OptionParser
-    for (num, arg) in enumerate(sys.argv[1:]):
-        if arg == '--entire-thread':
-            entire_thread = True
-        elif arg.startswith("--format="):
-            out_format = arg.split("=")[1]
-            if out_format == 'json':
-                # for compatibility use --entire-thread for json
-                  entire_thread = True
-            if not out_format in ("json", "text"):
-                  raise Exception("unknown format")
-        elif not arg.startswith('--'):
-            # save the position of the first sys.argv that is a search term
-            first_search_term = num + 1
-
-    if first_search_term:
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[first_search_term:])
-
-    threads = Query(db, querystr).search_threads()
-    first_toplevel = True
-    if out_format == "json":
-        sys.stdout.write("[")
-    for thread in threads:
-        msgs = thread.get_toplevel_messages()
-        if not first_toplevel:
-            if out_format == "json":
-                sys.stdout.write(", ")
-        first_toplevel = False
-        msgs.print_messages(out_format, 0, entire_thread)
-
-    if out_format == "json":
-        sys.stdout.write("]")
-    sys.stdout.write("\n")
-
-if __name__ == '__main__':
-    main()
diff --git a/bindings/python/notmuch/compat.py b/bindings/python/notmuch/compat.py
new file mode 100644 (file)
index 0000000..adc8d24
--- /dev/null
@@ -0,0 +1,67 @@
+'''
+This file is part of notmuch.
+
+This module handles differences between python2.x and python3.x and
+allows the notmuch bindings to support both version families with one
+source tree.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+Copyright 2012 Justus Winter <4winter@informatik.uni-hamburg.de>
+'''
+
+import sys
+
+if sys.version_info[0] == 2:
+    from ConfigParser import SafeConfigParser
+
+    class Python3StringMixIn(object):
+        def __str__(self):
+            return unicode(self).encode('utf-8')
+
+    def encode_utf8(value):
+        '''
+        Ensure a nicely utf-8 encoded string to pass to wrapped
+        libnotmuch functions.
+
+        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' % type(value))
+
+        if isinstance(value, unicode):
+            return value.encode('utf-8', 'replace')
+
+        return value
+else:
+    from configparser import SafeConfigParser
+
+    class Python3StringMixIn(object):
+        def __str__(self):
+            return self.__unicode__()
+
+    def encode_utf8(value):
+        '''
+        Ensure a nicely utf-8 encoded string to pass to wrapped
+        libnotmuch functions.
+
+        C++ code expects strings to be well formatted and unicode
+        strings to have no null bytes.
+        '''
+        if not isinstance(value, str):
+            raise TypeError('Expected str, got %s' % type(value))
+
+        return value.encode('utf-8', 'replace')
index e5c74cfb164557e53914559f758712b491a3d78d..fe692eb7ae4e38dceeb9aae24ca9b6cccb595a09 100644 (file)
@@ -20,7 +20,8 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 import os
 import codecs
 from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER
-from notmuch.globals import (
+from .compat import SafeConfigParser
+from .globals import (
     nmlib,
     Enum,
     _str,
@@ -37,8 +38,8 @@ from .errors import (
     NotInitializedError,
     ReadOnlyDatabaseError,
 )
-from notmuch.message import Message
-from notmuch.tag import Tags
+from .message import Message
+from .tag import Tags
 from .query import Query
 from .directory import Directory
 
@@ -546,7 +547,7 @@ class Database(object):
         """
         self._assert_db_is_initialized()
         tags_p = Database._get_all_tags(self._db)
-        if tags_p == None:
+        if not tags_p:
             raise NullPointerError()
         return Tags(tags_p, self)
 
@@ -577,13 +578,6 @@ class Database(object):
         """ Reads a user's notmuch config and returns his db location
 
         Throws a NotmuchError if it cannot find it"""
-        try:
-            # python3.x
-            from configparser import SafeConfigParser
-        except ImportError:
-            # python2.x
-            from ConfigParser import SafeConfigParser
-
         config = SafeConfigParser()
         conf_f = os.getenv('NOTMUCH_CONFIG',
                            os.path.expanduser('~/.notmuch-config'))
@@ -592,12 +586,3 @@ class Database(object):
             raise NotmuchError(message="No DB path specified"
                                        " and no user default found")
         return config.get('database', 'path')
-
-    @property
-    def db_p(self):
-        """Property returning a pointer to `notmuch_database_t` or `None`
-
-        This should normally not be needed by a user (and is not yet
-        guaranteed to remain stable in future versions).
-        """
-        return self._db
index ae115f818be2c22395531e454dc51ab907405f3c..3b0a525dc9083e2c6bcb6c019fa729f42d389fd7 100644 (file)
@@ -18,7 +18,7 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 """
 
 from ctypes import c_uint, c_long
-from notmuch.globals import (
+from .globals import (
     nmlib,
     NotmuchDirectoryP,
     NotmuchFilenamesP
index a0b2956316fdd79781905157b7bc1ea9d82dbd88..229f414de63224053b026a933a305d1ba9ad990b 100644 (file)
@@ -17,7 +17,7 @@ along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 """
 from ctypes import c_char_p
-from notmuch.globals import (
+from .globals import (
     nmlib,
     NotmuchMessageP,
     NotmuchFilenamesP,
index f5fad72aa4edf638f98b2508e0bce501fd2a9e55..c7632c3266d428cc42ee43c652701df13c7364dd 100644 (file)
@@ -16,7 +16,7 @@ along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
 
 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 """
-import sys
+
 from ctypes import CDLL, Structure, POINTER
 
 #-----------------------------------------------------------------------------
@@ -26,38 +26,7 @@ try:
 except:
     raise ImportError("Could not find shared 'notmuch' library.")
 
-
-if sys.version_info[0] == 2:
-    class Python3StringMixIn(object):
-        def __str__(self):
-            return unicode(self).encode('utf-8')
-
-
-    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" % type(value))
-        if isinstance(value, unicode):
-            return value.encode('UTF-8')
-        return value
-else:
-    class Python3StringMixIn(object):
-        def __str__(self):
-            return self.__unicode__()
-
-
-    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, str):
-            raise TypeError("Expected str, got %s" % type(value))
-        return value.encode('UTF-8')
-
+from .compat import Python3StringMixIn, encode_utf8 as _str
 
 class Enum(object):
     """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc..."""
index 0e65694e0c63e978754e5e6137a0475a33d3d3b8..d1c1b58c4a9a0cc129830fdcec2eceee93821d60 100644 (file)
@@ -41,10 +41,6 @@ from .tag import Tags
 from .filenames import Filenames
 
 import email
-try:
-    import simplejson as json
-except ImportError:
-    import json
 
 
 class Message(Python3StringMixIn):
@@ -304,7 +300,7 @@ class Message(Python3StringMixIn):
             raise NotInitializedError()
 
         tags_p = Message._get_tags(self._msg)
-        if tags_p == None:
+        if not tags_p:
             raise NullPointerError()
         return Tags(tags_p, self)
 
@@ -610,135 +606,6 @@ class Message(Python3StringMixIn):
             out_part = parts[(num - 1)]
             return out_part.get_payload(decode=True)
 
-    def format_message_internal(self):
-        """Create an internal representation of the message parts,
-        which can easily be output to json, text, or another output
-        format. The argument match tells whether this matched a
-        query.
-
-        .. deprecated:: 0.13
-                        This code adds functionality at the python
-                        level that is unlikely to be useful for
-                        anyone. Furthermore the python bindings strive
-                        to be a thin wrapper around libnotmuch, so
-                        this code will be removed in notmuch 0.14.
-        """
-        output = {}
-        output["id"] = self.get_message_id()
-        output["match"] = self.is_match()
-        output["filename"] = self.get_filename()
-        output["tags"] = list(self.get_tags())
-
-        headers = {}
-        for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
-            headers[h] = self.get_header(h)
-        output["headers"] = headers
-
-        body = []
-        parts = self.get_message_parts()
-        for i in xrange(len(parts)):
-            msg = parts[i]
-            part_dict = {}
-            part_dict["id"] = i + 1
-            # We'll be using this is a lot, so let's just get it once.
-            cont_type = msg.get_content_type()
-            part_dict["content-type"] = cont_type
-            # NOTE:
-            # Now we emulate the current behaviour, where it ignores
-            # 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
-            # incompatible solution.
-            disposition = msg["Content-Disposition"]
-            if disposition and disposition.lower().startswith("attachment"):
-                part_dict["filename"] = msg.get_filename()
-            else:
-                if cont_type.lower() == "text/plain":
-                    part_dict["content"] = msg.get_payload()
-                elif (cont_type.lower() == "text/html" and
-                      i == 0):
-                    part_dict["content"] = msg.get_payload()
-            body.append(part_dict)
-
-        output["body"] = body
-
-        return output
-
-    def format_message_as_json(self, indent=0):
-        """Outputs the message as json. This is essentially the same
-        as python's dict format, but we run it through, just so we
-        don't have to worry about the details.
-
-        .. deprecated:: 0.13
-                        This code adds functionality at the python
-                        level that is unlikely to be useful for
-                        anyone. Furthermore the python bindings strive
-                        to be a thin wrapper around libnotmuch, so
-                        this code will be removed in notmuch 0.14.
-        """
-        return json.dumps(self.format_message_internal())
-
-    def format_message_as_text(self, indent=0):
-        """Outputs it in the old-fashioned notmuch text form. Will be
-        easy to change to a new format when the format changes.
-
-        .. deprecated:: 0.13
-                        This code adds functionality at the python
-                        level that is unlikely to be useful for
-                        anyone. Furthermore the python bindings strive
-                        to be a thin wrapper around libnotmuch, so
-                        this code will be removed in notmuch 0.14.
-        """
-
-
-        format = self.format_message_internal()
-        output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
-                 % (format['id'], indent, format['match'], format['filename'])
-        output += "\n\fheader{"
-
-        #Todo: this date is supposed to be prettified, as in the index.
-        output += "\n%s (%s) (" % (format["headers"]["From"],
-                                   format["headers"]["Date"])
-        output += ", ".join(format["tags"])
-        output += ")"
-
-        output += "\nSubject: %s" % format["headers"]["Subject"]
-        output += "\nFrom: %s" % format["headers"]["From"]
-        output += "\nTo: %s" % format["headers"]["To"]
-        if format["headers"]["Cc"]:
-            output += "\nCc: %s" % format["headers"]["Cc"]
-        if format["headers"]["Bcc"]:
-            output += "\nBcc: %s" % format["headers"]["Bcc"]
-        output += "\nDate: %s" % format["headers"]["Date"]
-        output += "\n\fheader}"
-
-        output += "\n\fbody{"
-
-        parts = format["body"]
-        parts.sort(key=lambda x: x['id'])
-        for p in parts:
-            if not "filename" in p:
-                output += "\n\fpart{ "
-                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}"
-            else:
-                output += "\n\fattachment{ "
-                output += "ID: %d, Content-type:%s\n" % (p["id"],
-                                                         p["content-type"])
-                output += "Attachment: %s\n" % p["filename"]
-                output += "\n\fattachment}\n"
-
-        output += "\n\fbody}\n"
-        output += "\n\fmessage}"
-
-        return output
-
     def __hash__(self):
         """Implement hash(), so we can use Message() sets"""
         file = self.get_filename()
index 59ef40afee1ea2103f8273492c57b5debb4c9479..76100ffbc3b0b14dd42f3b1e4198f5327ee5ae1b 100644 (file)
@@ -31,8 +31,6 @@ from .errors import (
 from .tag import Tags
 from .message import Message
 
-import sys
-
 class Messages(object):
     """Represents a list of notmuch messages
 
@@ -142,7 +140,7 @@ class Messages(object):
         #reset _msgs as we iterated over it and can do so only once
         self._msgs = None
 
-        if tags_p == None:
+        if not tags_p:
             raise NullPointerError()
         return Tags(tags_p, self)
 
@@ -191,73 +189,6 @@ class Messages(object):
         if self._msgs:
             self._destroy(self._msgs)
 
-    def format_messages(self, format, indent=0, entire_thread=False):
-        """Formats messages as needed for 'notmuch show'.
-
-        :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
-                       whole threads or only the matching messages.
-        :return: a list of lines
-        """
-        result = list()
-
-        if format.lower() == "text":
-            set_start = ""
-            set_end = ""
-            set_sep = ""
-        elif format.lower() == "json":
-            set_start = "["
-            set_end = "]"
-            set_sep = ", "
-        else:
-            raise TypeError("format must be either 'text' or 'json'")
-
-        first_set = True
-
-        result.append(set_start)
-
-        # iterate through all toplevel messages in this thread
-        for msg in self:
-            # if not msg:
-            #     break
-            if not first_set:
-                result.append(set_sep)
-            first_set = False
-
-            result.append(set_start)
-            match = msg.is_match()
-            next_indent = indent
-
-            if (match or entire_thread):
-                if format.lower() == "text":
-                    result.append(msg.format_message_as_text(indent))
-                else:
-                    result.append(msg.format_message_as_json(indent))
-                next_indent = indent + 1
-
-            # get replies and print them also out (if there are any)
-            replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
-            if replies:
-                result.append(set_sep)
-                result.extend(replies)
-
-            result.append(set_end)
-        result.append(set_end)
-
-        return result
-
-    def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
-        """Outputs messages as needed for 'notmuch show' to a file like object.
-
-        :param format: A string of either 'text' or 'json'.
-        :param handle: A file like object to print to (default is sys.stdout).
-        :param indent: A number indicating the reply depth of these messages.
-        :param entire_thread: A bool, indicating whether we want to output
-                       whole threads or only the matching messages.
-        """
-        handle.write(''.join(self.format_messages(format, indent, entire_thread)))
-
 class EmptyMessagesResult(Messages):
     def __init__(self, parent):
         self._msgs = None
index 756e63b582a0ed6eeb06a131dfeb110e76215111..b11a399d2cf7f592922e1c09d06880c5b63d0ea2 100644 (file)
@@ -18,7 +18,7 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 """
 
 from ctypes import c_char_p, c_uint
-from notmuch.globals import (
+from .globals import (
     nmlib,
     Enum,
     _str,
@@ -100,7 +100,7 @@ class Query(object):
         # 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, _str(querystr))
+        query_p = Query._create(db._db, _str(querystr))
         if not query_p:
             raise NullPointerError
         self._query = query_p
index 363c3487fff0a2d8e06c354530f97256da89286b..1d52345794fcfef072d572ae04cfd37fa05dcab5 100644 (file)
@@ -17,7 +17,7 @@ along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 """
 from ctypes import c_char_p
-from notmuch.globals import (
+from .globals import (
     nmlib,
     Python3StringMixIn,
     NotmuchTagsP,
index 2f60d493fc52b0945e7479d9655d8b5c89edd7f6..009cb2bfb34c29e77f31d011bf596df593485032 100644 (file)
@@ -18,7 +18,7 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 """
 
 from ctypes import c_char_p, c_long, c_int
-from notmuch.globals import (
+from .globals import (
     nmlib,
     NotmuchThreadP,
     NotmuchMessagesP,
@@ -29,7 +29,7 @@ from .errors import (
     NotInitializedError,
 )
 from .messages import Messages
-from notmuch.tag import Tags
+from .tag import Tags
 from datetime import date
 
 class Thread(object):
@@ -238,7 +238,7 @@ class Thread(object):
             raise NotInitializedError()
 
         tags_p = Thread._get_tags(self._thread)
-        if tags_p == None:
+        if not tags_p:
             raise NullPointerError()
         return Tags(tags_p, self)
 
index d2e0a910e5794d3bf5b183dc436dfcd38a4005b9..f8ca34a9b14cc0f801a019848d4177a836cfc1a0 100644 (file)
@@ -17,7 +17,7 @@ along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 """
 
-from notmuch.globals import (
+from .globals import (
     nmlib,
     Python3StringMixIn,
     NotmuchThreadP,
index 90bcadbe67ce3a1db0c70cc53b5f8bdb5a4875b9..6a1b708403eecfc062b9516738bff50a6f3546ee 100644 (file)
@@ -1,2 +1,2 @@
 # this file should be kept in sync with ../../../version
-__VERSION__ = '0.13.2'
+__VERSION__ = '0.15.1'
index 3f9512ba903b9b9a9bb5c32c4be37696fc1cd2f8..fe81b3f96de5c86b1f4e28fdab3ed1f47d44236a 100644 (file)
 #include <notmuch.h>
 #include <ruby.h>
 
-VALUE notmuch_rb_cDatabase;
-VALUE notmuch_rb_cDirectory;
-VALUE notmuch_rb_cFileNames;
-VALUE notmuch_rb_cQuery;
-VALUE notmuch_rb_cThreads;
-VALUE notmuch_rb_cThread;
-VALUE notmuch_rb_cMessages;
-VALUE notmuch_rb_cMessage;
-VALUE notmuch_rb_cTags;
-
-VALUE notmuch_rb_eBaseError;
-VALUE notmuch_rb_eDatabaseError;
-VALUE notmuch_rb_eMemoryError;
-VALUE notmuch_rb_eReadOnlyError;
-VALUE notmuch_rb_eXapianError;
-VALUE notmuch_rb_eFileError;
-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;
+extern VALUE notmuch_rb_cDatabase;
+extern VALUE notmuch_rb_cDirectory;
+extern VALUE notmuch_rb_cFileNames;
+extern VALUE notmuch_rb_cQuery;
+extern VALUE notmuch_rb_cThreads;
+extern VALUE notmuch_rb_cThread;
+extern VALUE notmuch_rb_cMessages;
+extern VALUE notmuch_rb_cMessage;
+extern VALUE notmuch_rb_cTags;
+
+extern VALUE notmuch_rb_eBaseError;
+extern VALUE notmuch_rb_eDatabaseError;
+extern VALUE notmuch_rb_eMemoryError;
+extern VALUE notmuch_rb_eReadOnlyError;
+extern VALUE notmuch_rb_eXapianError;
+extern VALUE notmuch_rb_eFileError;
+extern VALUE notmuch_rb_eFileNotEmailError;
+extern VALUE notmuch_rb_eNullPointerError;
+extern VALUE notmuch_rb_eTagTooLongError;
+extern VALUE notmuch_rb_eUnbalancedFreezeThawError;
+extern VALUE notmuch_rb_eUnbalancedAtomicError;
+
+extern ID ID_call;
+extern ID ID_db_create;
+extern ID ID_db_mode;
 
 /* RSTRING_PTR() is new in ruby-1.9 */
 #if !defined(RSTRING_PTR)
index 3fe60fb7cc01efa147ca6b92fe4a1316c89bdeed..f4931d34a4f39cf321e225e6f21690328654dad9 100644 (file)
 
 #include "defs.h"
 
+VALUE notmuch_rb_cDatabase;
+VALUE notmuch_rb_cDirectory;
+VALUE notmuch_rb_cFileNames;
+VALUE notmuch_rb_cQuery;
+VALUE notmuch_rb_cThreads;
+VALUE notmuch_rb_cThread;
+VALUE notmuch_rb_cMessages;
+VALUE notmuch_rb_cMessage;
+VALUE notmuch_rb_cTags;
+
+VALUE notmuch_rb_eBaseError;
+VALUE notmuch_rb_eDatabaseError;
+VALUE notmuch_rb_eMemoryError;
+VALUE notmuch_rb_eReadOnlyError;
+VALUE notmuch_rb_eXapianError;
+VALUE notmuch_rb_eFileError;
+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;
+
 /*
  * Document-module: Notmuch
  *
index 76b185f85be6d04487f6a107be03d820bf6cd234..bf9aecabe86923e7dc3f560bd61f5e7119c5289b 100644 (file)
 */
 
 static notmuch_bool_t
-_process_keyword_arg (const notmuch_opt_desc_t *arg_desc, const char *arg_str) {
+_process_keyword_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
 
     const notmuch_keyword_t *keywords = arg_desc->keywords;
 
+    if (next == '\0') {
+       /* No keyword given */
+       arg_str = "";
+    }
+
     while (keywords->name) {
        if (strcmp (arg_str, keywords->name) == 0) {
            if (arg_desc->output_var) {
@@ -24,14 +29,17 @@ _process_keyword_arg (const notmuch_opt_desc_t *arg_desc, const char *arg_str) {
        }
        keywords++;
     }
-    fprintf (stderr, "unknown keyword: %s\n", arg_str);
+    if (next != '\0')
+       fprintf (stderr, "Unknown keyword argument \"%s\" for option \"%s\".\n", arg_str, arg_desc->name);
+    else
+       fprintf (stderr, "Option \"%s\" needs a keyword argument.\n", arg_desc->name);
     return FALSE;
 }
 
 static notmuch_bool_t
 _process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
 
-    if (next == 0) {
+    if (next == '\0') {
        *((notmuch_bool_t *)arg_desc->output_var) = TRUE;
        return TRUE;
     }
@@ -43,9 +51,43 @@ _process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next, const char
        *((notmuch_bool_t *)arg_desc->output_var) = TRUE;
        return TRUE;
     }
+    fprintf (stderr, "Unknown argument \"%s\" for (boolean) option \"%s\".\n", arg_str, arg_desc->name);
+    return FALSE;
+}
+
+static notmuch_bool_t
+_process_int_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
+
+    char *endptr;
+    if (next == '\0' || arg_str[0] == '\0') {
+       fprintf (stderr, "Option \"%s\" needs an integer argument.\n", arg_desc->name);
+       return FALSE;
+    }
+
+    *((int *)arg_desc->output_var) = strtol (arg_str, &endptr, 10);
+    if (*endptr == '\0')
+       return TRUE;
+
+    fprintf (stderr, "Unable to parse argument \"%s\" for option \"%s\" as an integer.\n",
+            arg_str, arg_desc->name);
     return FALSE;
 }
 
+static notmuch_bool_t
+_process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
+
+    if (next == '\0') {
+       fprintf (stderr, "Option \"%s\" needs a string argument.\n", arg_desc->name);
+       return FALSE;
+    }
+    if (arg_str[0] == '\0') {
+       fprintf (stderr, "String argument for option \"%s\" must be non-empty.\n", arg_desc->name);
+       return FALSE;
+    }
+    *((const char **)arg_desc->output_var) = arg_str;
+    return TRUE;
+}
+
 /*
    Search for the {pos_arg_index}th position argument, return FALSE if
    that does not exist.
@@ -85,43 +127,35 @@ parse_option (const char *arg,
 
     arg += 2;
 
-    const notmuch_opt_desc_t *try = options;
-    while (try->opt_type != NOTMUCH_OPT_END) {
+    const notmuch_opt_desc_t *try;
+    for (try = options; try->opt_type != NOTMUCH_OPT_END; try++) {
        if (try->name && strncmp (arg, try->name, strlen (try->name)) == 0) {
            char next = arg[strlen (try->name)];
            const char *value= arg+strlen(try->name)+1;
 
-           char *endptr;
-
-           /* Everything but boolean arguments (switches) needs a
-            * delimiter, and a non-zero length value. Boolean
-            * arguments may take an optional =true or =false value.
-            */
-           if (next != '=' && next != ':' && next != 0) return FALSE;
-           if (next == 0) {
-               if (try->opt_type != NOTMUCH_OPT_BOOLEAN)
-                   return FALSE;
-           } else {
-               if (value[0] == 0) return FALSE;
-           }
+           /* If we have not reached the end of the argument
+              (i.e. the next character is not a space or delimiter)
+              then the argument could still match a longer option
+              name later in the option table.
+           */
+           if (next != '=' && next != ':' && next != '\0')
+               continue;
 
            if (try->output_var == NULL)
                INTERNAL_ERROR ("output pointer NULL for option %s", try->name);
 
            switch (try->opt_type) {
            case NOTMUCH_OPT_KEYWORD:
-               return _process_keyword_arg (try, value);
+               return _process_keyword_arg (try, next, value);
                break;
            case NOTMUCH_OPT_BOOLEAN:
                return _process_boolean_arg (try, next, value);
                break;
            case NOTMUCH_OPT_INT:
-               *((int *)try->output_var) = strtol (value, &endptr, 10);
-               return (*endptr == 0);
+               return _process_int_arg (try, next, value);
                break;
            case NOTMUCH_OPT_STRING:
-               *((const char **)try->output_var) = value;
-               return TRUE;
+               return _process_string_arg (try, next, value);
                break;
            case NOTMUCH_OPT_POSITION:
            case NOTMUCH_OPT_END:
@@ -130,7 +164,6 @@ parse_option (const char *arg,
                /*UNREACHED*/
            }
        }
-       try++;
     }
     fprintf (stderr, "Unrecognized option: --%s\n", arg);
     return FALSE;
index 38e2e146b07e0d1582677fde256e4a3a8fe1c47d..12aacf42772c3c2986f955dd17fe26b4f04bab48 100644 (file)
@@ -1,6 +1,6 @@
 notmuch/compat
 
-This directory consists of two things:
+This directory consists of three things:
 
 1. Small programs used by the notmuch configure script to test for the
    availability of certain system features, (library functions, etc.).
@@ -14,3 +14,8 @@ This directory consists of two things:
 
    The compilation of these files is made conditional on the output of
    the test programs from [1].
+
+3. Macro definitions abstracting compiler differences (e.g. function
+   attributes).
+
+   For example: function-attributes.h
diff --git a/compat/function-attributes.h b/compat/function-attributes.h
new file mode 100644 (file)
index 0000000..8450a17
--- /dev/null
@@ -0,0 +1,47 @@
+/* function-attributes.h - Provides compiler abstractions for
+ *                         function attributes
+ *
+ * Copyright (c) 2012 Justus Winter <4winter@informatik.uni-hamburg.de>
+ *
+ * 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/ .
+ */
+
+#ifndef FUNCTION_ATTRIBUTES_H
+#define FUNCTION_ATTRIBUTES_H
+
+/* clang provides this macro to test for support for function
+ * attributes. If it isn't defined, this provides a compatibility
+ * macro for other compilers.
+ */
+#ifndef __has_attribute
+#define __has_attribute(x) 0
+#endif
+
+/* Provide a NORETURN_ATTRIBUTE macro similar to PRINTF_ATTRIBUTE from
+ * talloc.
+ *
+ * This attribute is understood by gcc since version 2.5. clang
+ * provides support for testing for function attributes.
+ */
+#ifndef NORETURN_ATTRIBUTE
+#if (__GNUC__ >= 3 ||                          \
+     (__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || \
+     __has_attribute (noreturn))
+#define NORETURN_ATTRIBUTE __attribute__ ((noreturn))
+#else
+#define NORETURN_ATTRIBUTE
+#endif
+#endif
+
+#endif
index 71981b7c614a58f6b916acd49cf6515253dce396..460fcfcf31178ea4b1ecdf9fc506e692b064747a 100755 (executable)
--- a/configure
+++ b/configure
@@ -1,7 +1,21 @@
 #! /bin/sh
 
+# Test whether this shell is capable of parameter substring processing.
+( option='a/b'; : ${option#*/} ) 2>/dev/null || {
+    echo "
+The shell interpreting '$0' is lacking some required features.
+
+To work around this problem you may try to execute:
+
+    ksh $0 $*
+ or
+    bash $0 $*
+"
+    exit 1
+}
+
 # Store original IFS value so it can be changed (and restored) in many places.
-readonly DEFAULT_IFS=$IFS
+readonly DEFAULT_IFS="$IFS"
 
 srcdir=$(dirname "$0")
 
@@ -114,6 +128,10 @@ Fine tuning of some installation directories is available:
        --bashcompletiondir=DIR Bash completions files [SYSCONFDIR/bash_completion.d]
        --zshcompletiondir=DIR  Zsh completions files [PREFIX/share/zsh/functions/Completion/Unix]
 
+Some specific library versions can be specified (auto-detected otherwise):
+
+        --with-gmime-version=VERS       Specify GMIME version (2.4 or 2.6)
+
 Some features can be disabled (--with-feature=no is equivalent to
 --without-feature) :
 
@@ -218,7 +236,12 @@ done
 # Makefile.config file later like most values), because we need to
 # actually investigate this value compared to the ldconfig_paths value
 # below.
-libdir_expanded=${LIBDIR:-${PREFIX}/lib}
+if [ -z "$LIBDIR" ] ; then
+    libdir_expanded="${PREFIX}/lib"
+else
+    # very non-general variable expansion
+    libdir_expanded=`echo "$LIBDIR" | sed "s|\\${prefix}|${PREFIX}|g; s|\\$prefix/|${PREFIX}/|; s|//*|/|g"`
+fi
 
 cat <<EOF
 Welcome to Notmuch, a system for indexing, searching and tagging your email.
@@ -356,6 +379,14 @@ elif [ $uname = "SunOS" ] ; then
     printf "Solaris.\n"
     platform=SOLARIS
     linker_resolves_library_dependencies=0
+elif [ $uname = "FreeBSD" ] ; then
+    printf "FreeBSD.\n"
+    platform=FREEBSD
+    linker_resolves_library_dependencies=0
+elif [ $uname = "OpenBSD" ] ; then
+    printf "OpenBSD.\n"
+    platform=OPENBSD
+    linker_resolves_library_dependencies=0
 elif [ $uname = "Linux" ] || [ $uname = "GNU" ] ; then
     printf "$uname\n"
     platform="$uname"
@@ -510,7 +541,7 @@ fi
 
 WARN_CXXFLAGS=""
 printf "Checking for available C++ compiler warning flags... "
-for flag in -Wall -Wextra -Wwrite-strings -Wswitch-enum; do
+for flag in -Wall -Wextra -Wwrite-strings; do
     if ${CC} $flag -o minimal minimal.c > /dev/null 2>&1
     then
        WARN_CXXFLAGS="${WARN_CXXFLAGS}${WARN_CXXFLAGS:+ }${flag}"
@@ -645,7 +676,7 @@ HAVE_GETLINE = ${have_getline}
 # build its own version)
 HAVE_STRCASESTR = ${have_strcasestr}
 
-# Supported platforms (so far) are: LINUX, MACOSX, SOLARIS
+# Supported platforms (so far) are: LINUX, MACOSX, SOLARIS, FREEBSD, OPENBSD
 PLATFORM = ${platform}
 
 # Whether the linker will automatically resolve the dependency of one
diff --git a/contrib/nmbug b/contrib/nmbug
deleted file mode 100755 (executable)
index bb0739f..0000000
+++ /dev/null
@@ -1,621 +0,0 @@
-#!/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/nmbug/nmbug b/contrib/nmbug/nmbug
new file mode 100755 (executable)
index 0000000..f003ef9
--- /dev/null
@@ -0,0 +1,648 @@
+#!/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>);
+  unless (close $fh) {
+    die "'git @_' exited with nonzero value\n";
+  }
+  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 ! close $fh;
+    } 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/);
+  }
+  unless (close $fh) {
+    die "'notmuch search --output=tags *' exited with nonzero value\n";
+  }
+  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});
+  }
+  unless (close $git) {
+    die "'git update-index --index-info' exited with nonzero value\n";
+  }
+
+}
+
+
+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);
+  }
+  unless (close $git) {
+    die "'git update-index --index-info' exited with nonzero value\n";
+  }
+  unless (close $fh) {
+    die "'notmuch dump -- $query' exited with nonzero value\n";
+  }
+  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;
+    }
+    unless (close $fh) {
+      die "'notmuch search --output=files id:$id' exited with nonzero value\n";
+    }
+  }
+
+
+  @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/ );
+
+  my @lines = unpack_diff_lines ($fh);
+  unless (close $fh) {
+    die "'git diff-index --cached --diff-filter=$filter --name-only HEAD' ",
+       "exited with nonzero value\n";
+  }
+  return @lines;
+}
+
+
+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);
+
+  my @lines = unpack_diff_lines ($fh);
+  unless (close $fh) {
+    die "'git diff --diff-filter=$filter --name-only $ref1 $ref2' ",
+       "exited with nonzero value\n";
+  }
+  return @lines;
+}
+
+
+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/nmbug/nmbug-status b/contrib/nmbug/nmbug-status
new file mode 100755 (executable)
index 0000000..d08ca08
--- /dev/null
@@ -0,0 +1,176 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2011-2012 David Bremner <david@tethera.net>
+# License: Same as notmuch
+# dependencies
+#       - python 2.6 for json
+#       - argparse; either python 2.7, or install separately
+
+import datetime
+import notmuch
+import rfc822
+import urllib
+import json
+import argparse
+import os
+import subprocess
+
+# parse command line arguments
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--text', help='output plain text format',
+                    action='store_true')
+
+parser.add_argument('--config', help='load config from given file')
+
+
+args = parser.parse_args()
+
+# read config from json file
+
+if args.config != None:
+    fp = open(args.config)
+else:
+    nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
+
+    # read only the first line from the pipe
+    sha1 = subprocess.Popen(['git', '--git-dir', nmbhome,
+                             'show-ref', '-s', 'config'],
+                            stdout=subprocess.PIPE).stdout.readline()
+
+    sha1 = sha1.rstrip()
+
+    fp = subprocess.Popen(['git', '--git-dir', nmbhome,
+                           'cat-file', 'blob', sha1+':status-config.json'],
+                          stdout=subprocess.PIPE).stdout
+
+config = json.load(fp)
+
+if args.text:
+    output_format = 'text'
+else:
+    output_format = 'html'
+
+class Thread:
+    def __init__(self, last, lines):
+        self.last = last
+        self.lines = lines
+
+    def join_utf8_with_newlines(self):
+        return '\n'.join( (line.encode('utf-8') for line in self.lines) )
+
+def output_with_separator(threadlist, sep):
+    outputs = (thread.join_utf8_with_newlines() for thread in threadlist)
+    print sep.join(outputs)
+
+headers = ['date', 'from', 'subject']
+
+def print_view(title, query, comment):
+
+    query_string = ' and '.join(query)
+    q_new = notmuch.Query(db, query_string)
+    q_new.set_sort(notmuch.Query.SORT.OLDEST_FIRST)
+
+    last_thread_id = ''
+    threads = {}
+    threadlist = []
+    out = {}
+    last = None
+    lines = None
+
+    if output_format == 'html':
+        print '<h3><a name="%s" />%s</h3>' % (title, title)
+        print comment
+        print 'The view is generated from the following query:'
+        print '<blockquote>'
+        print query_string
+        print '</blockquote>'
+        print '<table>\n'
+
+    for m in q_new.search_messages():
+
+        thread_id = m.get_thread_id()
+
+        if thread_id != last_thread_id:
+            if threads.has_key(thread_id):
+                last = threads[thread_id].last
+                lines = threads[thread_id].lines
+            else:
+                last = {}
+                lines = []
+                thread = Thread(last, lines)
+                threads[thread_id] = thread
+                for h in headers:
+                    last[h] = ''
+                threadlist.append(thread)
+            last_thread_id = thread_id
+
+        for header in headers:
+            val = m.get_header(header)
+
+            if header == 'date':
+                val = str.join(' ', val.split(None)[1:4])
+                val = str(datetime.datetime.strptime(val, '%d %b %Y').date())
+            elif header == 'from':
+                (val, addr) = rfc822.parseaddr(val)
+                if val == '':
+                    val = addr.split('@')[0]
+
+            if header != 'subject' and last[header] == val:
+                out[header] = ''
+            else:
+                out[header] = val
+                last[header] = val
+
+        mid = m.get_message_id()
+        out['id'] = 'id:"%s"' % mid
+
+        if output_format == 'html':
+
+            out['subject'] = '<a href="http://mid.gmane.org/%s">%s</a>' \
+                % (urllib.quote(mid), out['subject'])
+
+            lines.append(' <tr><td>%s' % out['date'])
+            lines.append('</td><td>%s' % out['id'])
+            lines.append('</td></tr>')
+            lines.append(' <tr><td>%s' % out['from'])
+            lines.append('</td><td>%s' % out['subject'])
+            lines.append('</td></tr>')
+        else:
+            lines.append('%(date)-10.10s %(from)-20.20s %(subject)-40.40s\n%(id)72s' % out)
+
+    if output_format == 'html':
+        output_with_separator(threadlist,
+                              '\n<tr><td colspan="2"><br /></td></tr>\n')
+        print '</table>'
+    else:
+        output_with_separator(threadlist, '\n\n')
+
+# main program
+
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE)
+
+if output_format == 'html':
+    print '''<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>Notmuch Patches</title>
+</head>
+<body>'''
+    print '<h2>Notmuch Patches</h2>'
+    print 'Generated: %s<br />' % datetime.datetime.utcnow().date()
+    print 'For more infomation see <a href="http://notmuchmail.org/nmbug">nmbug</a>'
+
+    print '<h3>Views</h3>'
+    print '<ul>'
+    for view in config['views']:
+        print '<li><a href="#%(title)s">%(title)s</a></li>' % view
+    print '</ul>'
+
+for view in config['views']:
+    print_view(**view)
+
+if output_format == 'html':
+    print '</body>\n</html>'
diff --git a/contrib/nmbug/status-config.json b/contrib/nmbug/status-config.json
new file mode 100644 (file)
index 0000000..6b4934f
--- /dev/null
@@ -0,0 +1,65 @@
+{
+    "views": [
+       {
+           "comment": "Unresolved bugs (or just need tag updating).",
+           "query": [
+               "tag:notmuch::bug",
+               "not tag:notmuch::fixed",
+               "not tag:notmuch::wontfix"
+           ],
+           "title": "Bugs"
+       },
+       {
+           "comment": "These patches are under consideration for pushing.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::python",
+               "not tag:notmuch::vim",
+               "not tag:notmuch::wontfix",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (Core and Emacs)"
+       },
+       {
+           "comment": "These python related patches might be ready to push, or they might just need updated tags.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::wontfix",
+               " tag:notmuch::python",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (Python)"
+       },
+       {
+           "comment": "These vim related patches might be ready to push, or they might just need updated tags.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::wontfix",
+               "tag:notmuch::vim",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (vim)"
+       },
+       {
+           "comment": "These patches are under review, or waiting for feedback.",
+           "query": [
+               "tag:notmuch::patch",
+               "not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete",
+               "not tag:notmuch::stale",
+               "not tag:notmuch::wontfix",
+               "(tag:notmuch::moreinfo or tag:notmuch::needs-review)"
+           ],
+           "title": "Review"
+       }
+    ]
+}
index 382ac911fd188c4b52e5f33cf98950a38887954b..e00035c62ecea05842d4f6bbe0a4ca9331c9f3c3 100644 (file)
@@ -41,6 +41,11 @@ To *run* notmuch-mutt you will need Perl with the following libraries:
   (Debian package: libstring-shellquote-perl)
 - Term::ReadLine <http://search.cpan.org/~hayashi/Term-ReadLine-Gnu/>
   (Debian package: libterm-readline-gnu-perl)
+- File::Which <http://search.cpan.org/dist/File-Which/>
+  (Debian package: libfile-which-perl)
+
+The --remove-dups option will use fdupes <https://code.google.com/p/fdupes/>
+if it is installed.  Version fdupes-1.50-PR2 or higher is required.
 
 To *build* notmuch-mutt documentation you will need:
 
index 71206c35269be8d221bbb038f4a00fa0cff26102..d14709df6eb3bccdd054a18f0ffa1cc984167d00 100755 (executable)
@@ -18,6 +18,8 @@ use Mail::Box::Maildir;
 use Pod::Usage;
 use String::ShellQuote;
 use Term::ReadLine;
+use Digest::SHA;
+use File::Which;
 
 
 my $xdg_cache_dir = "$ENV{HOME}/.cache";
@@ -34,16 +36,65 @@ sub empty_maildir($) {
     $folder->close();
 }
 
-# search($maildir, $query)
+# Match files by size and SHA-256; then delete duplicates
+sub builtin_remove_dups($) {
+    my ($maildir) = @_;
+    my (%size_to_files, %sha_to_files);
+
+    # Group files by matching sizes
+    foreach my $file (glob("$maildir/cur/*")) {
+        my $size = -s $file;
+        push(@{$size_to_files{$size}}, $file) if $size;
+    }
+
+    foreach my $same_size_files (values %size_to_files) {
+        # Don't run sha unless there is another file of the same size
+        next if scalar(@$same_size_files) < 2;
+        %sha_to_files = ();
+
+        # Group files with matching sizes by SHA-256
+        foreach my $file (@$same_size_files) {
+            open(my $fh, '<', $file) or next;
+            binmode($fh);
+            my $sha256hash = Digest::SHA->new(256)->addfile($fh)->hexdigest;
+            close($fh);
+
+            push(@{$sha_to_files{$sha256hash}}, $file);
+        }
+
+        # Remove duplicates
+        foreach my $same_sha_files (values %sha_to_files) {
+            next if scalar(@$same_sha_files) < 2;
+            unlink(@{$same_sha_files}[1..$#$same_sha_files]);
+        }
+    }
+}
+
+# Use either fdupes or the built-in scanner to detect and remove duplicate
+# search results in the maildir
+sub remove_duplicates($) {
+    my ($maildir) = @_;
+
+    my $fdupes = which("fdupes");
+    if ($fdupes) {
+      system("$fdupes --hardlinks --symlinks --delete --noprompt"
+             . " --quiet $maildir/cur/ > /dev/null");
+    } else {
+        builtin_remove_dups($maildir);
+    }
+}
+
+# search($maildir, $remove_dups, $query)
 # search mails according to $query with notmuch; store results in $maildir
-sub search($$) {
-    my ($maildir, $query) = @_;
+sub search($$$) {
+    my ($maildir, $remove_dups, $query) = @_;
     $query = shell_quote($query);
 
     empty_maildir($maildir);
     system("notmuch search --output=files $query"
           . " | sed -e 's: :\\\\ :g'"
           . " | xargs --no-run-if-empty ln -s -t $maildir/cur/");
+    remove_duplicates($maildir) if ($remove_dups);
 }
 
 sub prompt($$) {
@@ -60,7 +111,7 @@ sub prompt($$) {
     while (1) {
        chomp($query = $term->readline($text, $default));
        if ($query eq "?") {
-           system("man", "notmuch");
+           system("man", "notmuch-search-terms");
        } else {
            $term->WriteHistory($histfile);
            return $query;
@@ -74,28 +125,28 @@ sub get_message_id() {
     return $1;
 }
 
-sub search_action($$@) {
-    my ($interactive, $results_dir, @params) = @_;
+sub search_action($$$@) {
+    my ($interactive, $results_dir, $remove_dups, @params) = @_;
 
     if (! $interactive) {
-       search($results_dir, join(' ', @params));
+       search($results_dir, $remove_dups, join(' ', @params));
     } else {
        my $query = prompt("search ('?' for man): ", join(' ', @params));
        if ($query ne "") {
-           search($results_dir,$query);
+           search($results_dir, $remove_dups, $query);
        }
     }
 }
 
-sub thread_action(@) {
-    my ($results_dir, @params) = @_;
+sub thread_action($$@) {
+    my ($results_dir, $remove_dups, @params) = @_;
 
     my $mid = get_message_id();
     my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
     my $tid = `$search_cmd`;   # get thread id
     chomp($tid);
 
-    search($results_dir, $tid);
+    search($results_dir, $remove_dups, $tid);
 }
 
 sub tag_action(@) {
@@ -118,11 +169,13 @@ sub main() {
     my $results_dir = "$cache_dir/results";
     my $interactive = 0;
     my $help_needed = 0;
+    my $remove_dups = 0;
 
     my $getopt = GetOptions(
        "h|help" => \$help_needed,
        "o|output-dir=s" => \$results_dir,
-       "p|prompt" => \$interactive);
+       "p|prompt" => \$interactive,
+       "r|remove-dups" => \$remove_dups);
     if (! $getopt || $#ARGV < 0) { die_usage() };
     my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
 
@@ -136,9 +189,9 @@ sub main() {
        print STDERR "Error: no search term provided\n\n";
        die_usage();
     } elsif ($action eq "search") {
-       search_action($interactive, $results_dir, @params);
+       search_action($interactive, $results_dir, $remove_dups, @params);
     } elsif ($action eq "thread") {
-       thread_action($results_dir, @params);
+       thread_action($results_dir, $remove_dups, @params);
     } elsif ($action eq "tag") {
        tag_action(@params);
     } else {
@@ -189,6 +242,12 @@ be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
 Instead of using command line search terms, prompt the user for them (only for
 "search").
 
+=item -r
+
+=item --remove-dups
+
+Remove duplicates from search results.
+
 =item -h
 
 =item --help
@@ -205,13 +264,13 @@ the following in your Mutt configuration (usually one of: F<~/.muttrc>,
 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
 
     macro index <F8> \
-          "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt --prompt search<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter>" \
+          "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt -r --prompt search<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter>" \
           "notmuch: search mail"
     macro index <F9> \
-          "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt thread<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" \
+          "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt -r thread<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" \
           "notmuch: reconstruct thread"
     macro index <F6> \
-          "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -inbox<enter>" \
+          "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -- -inbox<enter>" \
           "notmuch: remove message from inbox"
 
 The first macro (activated by <F8>) prompts the user for notmuch search terms
index c0ff000b67cb4f6137c65280aa59d59bf3591666..ddc4b480e23c7804d573d74bc97dd93c0392e339 100644 (file)
@@ -1,9 +1,9 @@
 macro index <F8> \
-      "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt --prompt search<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>" \
+      "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt -r --prompt search<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>" \
       "notmuch: search mail"
 macro index <F9> \
-      "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt thread<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter><enter-command>set wait_key<enter>" \
+      "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt -r thread<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter><enter-command>set wait_key<enter>" \
       "notmuch: reconstruct thread"
 macro index <F6> \
-      "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -inbox<enter>" \
+      "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -- -inbox<enter>" \
       "notmuch: remove message from inbox"
diff --git a/contrib/notmuch-pick/README b/contrib/notmuch-pick/README
new file mode 100644 (file)
index 0000000..4200824
--- /dev/null
@@ -0,0 +1,43 @@
+NOTMUCH PICK
+
+Notmuch pick is an experimental threaded message view for the emacs
+interface. Each message is one line in the results and the thread
+structure is shown using UTF-8 box drawing characters (similar to
+Mutt's threaded view). It comes between search and show in terms of
+amount of output and can be useful for viewing both single threads and
+multiple threads.
+
+INSTALL
+
+Just copy the notmuch-pick.el file somewhere into emacs's load-path.
+
+Then after the "(require 'notmuch)" line in your .emacs file add
+the line "(require 'notmuch-pick nil t)". This will load notmuch-pick on
+your next emacs start.
+
+TEST
+
+Just execute run-tests.sh and it should all work (it does require that
+notmuch has already been built).
+
+USING PICK
+
+The main key entries to notmuch pick are
+
+'z' enter a query to view using notmuch pick (works in hello, search,
+    show and pick itself).
+'Z' view the current query in pick (works from search and show)
+'M-RET' view the selected thread in pick (works in search mode)
+
+Once in pick mode, keybindings are mostly in line with the rest of
+notmuch and are all viewable with '?' as usual.
+
+CUSTOMISING PICK
+
+Pick has several customisation variables. The most significant is the
+first notmuch-pick-show-out which determines the behaviour when
+selecting a message (with RET) in the pick view. By default pick uses
+a split window showing the single message in the bottom pane. However,
+if this option is set then it views the whole thread in the complete
+window jumping to the selected message in the thread. In either case
+M-RET selects the other option.
diff --git a/contrib/notmuch-pick/TODO b/contrib/notmuch-pick/TODO
new file mode 100644 (file)
index 0000000..8474e30
--- /dev/null
@@ -0,0 +1,29 @@
+TODO FOR NOTMUCH-PICK
+
+(These are the things I can think of: to be added to as problems get
+reported or found!)
+
+Things that need fixing before acceptance to mainline
+
+- Review lisp to make idiomatic.
+- Unify functions with search or show where appropriate.
+- Work out a fall-back if the font does not contain box graphic characters.
+- Add extra functionality?
+
+- Remove debugging information.
+
+- Add tests (I have some but I am not sure how to add them if pick is
+  in contrib).
+
+Bugs:
+
+- The display flickers while pick is running. I have no idea why.
+
+Other todo items
+
+- c i, c f for stashing ids etc.
+
+- Perhaps the author should be "To: ???" if the message is from the user.
+
+- Is there some nice way to do use the expand citation buttons of
+  notmuch-show when using the split-pane mode?
diff --git a/contrib/notmuch-pick/notmuch-pick.el b/contrib/notmuch-pick/notmuch-pick.el
new file mode 100644 (file)
index 0000000..d75a66a
--- /dev/null
@@ -0,0 +1,830 @@
+;; notmuch-pick.el --- displaying notmuch forests.
+;;
+;; Copyright © Carl Worth
+;; Copyright © David Edmondson
+;; Copyright © Mark Walters
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+;;          Mark Walters <markwalters1009@gmail.com>
+
+(require 'mail-parse)
+
+(require 'notmuch-lib)
+(require 'notmuch-query)
+(require 'notmuch-show)
+(require 'notmuch) ;; XXX ATM, as notmuch-search-mode-map is defined here
+
+(eval-when-compile (require 'cl))
+
+(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
+(declare-function notmuch-show "notmuch-show" (&rest args))
+(declare-function notmuch-tag "notmuch" (query &rest tags))
+(declare-function notmuch-show-strip-re "notmuch-show" (subject))
+(declare-function notmuch-show-spaces-n "notmuch-show" (n))
+(declare-function notmuch-read-query "notmuch" (prompt))
+(declare-function notmuch-read-tag-changes "notmuch" (&optional initial-input &rest search-terms))
+(declare-function notmuch-update-tags "notmuch" (current-tags tag-changes))
+(declare-function notmuch-hello-trim "notmuch-hello" (search))
+(declare-function notmuch-search-find-thread-id "notmuch" ())
+(declare-function notmuch-search-find-subject "notmuch" ())
+
+;; the following variable is defined in notmuch.el
+(defvar notmuch-search-query-string)
+
+(defgroup notmuch-pick nil
+  "Showing message and thread structure."
+  :group 'notmuch)
+
+;; This is ugly. We can't run setup-show-out until it has been defined
+;; which needs the keymap to be defined. So we defer setting up to
+;; notmuch-pick-init.
+(defcustom notmuch-pick-show-out nil
+  "View selected messages in new window rather than split-pane."
+  :type 'boolean
+  :group 'notmuch-pick
+  :set (lambda (symbol value)
+        (set-default symbol value)
+        (when (fboundp 'notmuch-pick-setup-show-out)
+          (notmuch-pick-setup-show-out))))
+
+(defcustom notmuch-pick-result-format
+  `(("date" . "%12s  ")
+    ("authors" . "%-20s")
+    ("subject" . " %-54s ")
+    ("tags" . "(%s)"))
+  "Result formatting for Pick. Supported fields are: date,
+        authors, subject, tags Note: subject includes the tree
+        structure graphics, and the author string should not
+        contain whitespace (put it in the neighbouring fields
+        instead).  For example:
+        (setq notmuch-pick-result-format \(\(\"authors\" . \"%-40s\"\)
+                                             \(\"subject\" . \"%s\"\)\)\)"
+  :type '(alist :key-type (string) :value-type (string))
+  :group 'notmuch-pick)
+
+(defcustom notmuch-pick-asynchronous-parser t
+  "Use the asynchronous parser."
+  :type 'boolean
+  :group 'notmuch-pick)
+
+;; Faces for messages that match the query.
+(defface notmuch-pick-match-date-face
+  '((t :inherit default))
+  "Face used in pick mode for the date in messages matching the query."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+(defface notmuch-pick-match-author-face
+  '((((class color)
+      (background dark))
+     (:foreground "OliveDrab1"))
+    (((class color)
+      (background light))
+     (:foreground "dark blue"))
+    (t
+     (:bold t)))
+  "Face used in pick mode for the date in messages matching the query."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+(defface notmuch-pick-match-subject-face
+  '((t :inherit default))
+  "Face used in pick mode for the subject in messages matching the query."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+(defface notmuch-pick-match-tag-face
+  '((((class color)
+      (background dark))
+     (:foreground "OliveDrab1"))
+    (((class color)
+      (background light))
+     (:foreground "navy blue" :bold t))
+    (t
+     (:bold t)))
+  "Face used in pick mode for tags in messages matching the query."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+;; Faces for messages that do not match the query.
+(defface notmuch-pick-no-match-date-face
+  '((t (:foreground "gray")))
+  "Face used in pick mode for non-matching dates."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+(defface notmuch-pick-no-match-subject-face
+  '((t (:foreground "gray")))
+  "Face used in pick mode for non-matching subjects."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+(defface notmuch-pick-no-match-author-face
+  '((t (:foreground "gray")))
+  "Face used in pick mode for the date in messages matching the query."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+(defface notmuch-pick-no-match-tag-face
+  '((t (:foreground "gray")))
+  "Face used in pick mode face for non-matching tags."
+  :group 'notmuch-pick
+  :group 'notmuch-faces)
+
+(defvar notmuch-pick-previous-subject "")
+(make-variable-buffer-local 'notmuch-pick-previous-subject)
+
+;; The basic query i.e. the key part of the search request.
+(defvar notmuch-pick-basic-query nil)
+(make-variable-buffer-local 'notmuch-pick-basic-query)
+;; The context of the search: i.e., useful but can be dropped.
+(defvar notmuch-pick-query-context nil)
+(make-variable-buffer-local 'notmuch-pick-query-context)
+(defvar notmuch-pick-buffer-name nil)
+(make-variable-buffer-local 'notmuch-pick-buffer-name)
+;; This variable is the window used for the message pane. It is set
+;; in both the parent pick buffer and the child show buffer. It is
+;; used to try and close the message pane when quitting pick or the
+;; child show buffer.
+(defvar notmuch-pick-message-window nil)
+(make-variable-buffer-local 'notmuch-pick-message-window)
+(put 'notmuch-pick-message-window 'permanent-local t)
+(defvar notmuch-pick-message-buffer nil)
+(make-variable-buffer-local 'notmuch-pick-message-buffer-name)
+(put 'notmuch-pick-message-buffer-name 'permanent-local t)
+(defvar notmuch-pick-process-state nil
+  "Parsing state of the search process filter.")
+
+
+(defvar notmuch-pick-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map [mouse-1] 'notmuch-pick-show-message)
+    (define-key map "q" 'notmuch-pick-quit)
+    (define-key map "x" 'notmuch-pick-quit)
+    (define-key map "?" 'notmuch-help)
+    (define-key map "a" 'notmuch-pick-archive-message-then-next)
+    (define-key map "=" 'notmuch-pick-refresh-view)
+    (define-key map "s" 'notmuch-pick-to-search)
+    (define-key map "z" 'notmuch-pick-to-pick)
+    (define-key map "m" 'notmuch-pick-new-mail)
+    (define-key map "f" 'notmuch-pick-forward-message)
+    (define-key map "r" 'notmuch-pick-reply-sender)
+    (define-key map "R" 'notmuch-pick-reply)
+    (define-key map "n" 'notmuch-pick-next-matching-message)
+    (define-key map "p" 'notmuch-pick-prev-matching-message)
+    (define-key map "N" 'notmuch-pick-next-message)
+    (define-key map "P" 'notmuch-pick-prev-message)
+    (define-key map "|" 'notmuch-pick-pipe-message)
+    (define-key map "-" 'notmuch-pick-remove-tag)
+    (define-key map "+" 'notmuch-pick-add-tag)
+    (define-key map " " 'notmuch-pick-scroll-or-next)
+    (define-key map "b" 'notmuch-pick-scroll-message-window-back)
+    map))
+(fset 'notmuch-pick-mode-map notmuch-pick-mode-map)
+
+(defun notmuch-pick-setup-show-out ()
+  (let ((map notmuch-pick-mode-map))
+    (if notmuch-pick-show-out
+       (progn
+         (define-key map (kbd "M-RET") 'notmuch-pick-show-message)
+         (define-key map (kbd "RET") 'notmuch-pick-show-message-out))
+      (progn
+       (define-key map (kbd "RET") 'notmuch-pick-show-message)
+       (define-key map (kbd "M-RET") 'notmuch-pick-show-message-out)))))
+
+(defun notmuch-pick-get-message-properties ()
+  "Return the properties of the current message as a plist.
+
+Some useful entries are:
+:headers - Property list containing the headers :Date, :Subject, :From, etc.
+:tags - Tags for this message"
+  (save-excursion
+    (beginning-of-line)
+    (get-text-property (point) :notmuch-message-properties)))
+
+(defun notmuch-pick-set-message-properties (props)
+  (save-excursion
+    (beginning-of-line)
+    (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
+
+(defun notmuch-pick-set-prop (prop val &optional props)
+  (let ((inhibit-read-only t)
+       (props (or props
+                  (notmuch-pick-get-message-properties))))
+    (plist-put props prop val)
+    (notmuch-pick-set-message-properties props)))
+
+(defun notmuch-pick-get-prop (prop &optional props)
+  (let ((props (or props
+                  (notmuch-pick-get-message-properties))))
+    (plist-get props prop)))
+
+(defun notmuch-pick-set-tags (tags)
+  "Set the tags of the current message."
+  (notmuch-pick-set-prop :tags tags))
+
+(defun notmuch-pick-get-tags ()
+  "Return the tags of the current message."
+  (notmuch-pick-get-prop :tags))
+
+(defun notmuch-pick-get-message-id ()
+  "Return the message id of the current message."
+  (let ((id (notmuch-pick-get-prop :id)))
+    (if id
+       (notmuch-id-to-query id)
+      nil)))
+
+(defun notmuch-pick-get-match ()
+  "Return whether the current message is a match."
+  (interactive)
+  (notmuch-pick-get-prop :match))
+
+(defun notmuch-pick-refresh-result ()
+  (let ((init-point (point))
+       (end (line-end-position))
+       (msg (notmuch-pick-get-message-properties))
+       (inhibit-read-only t))
+    (beginning-of-line)
+    (delete-region (point) (1+ (line-end-position)))
+    (notmuch-pick-insert-msg msg)
+    (let ((new-end (line-end-position)))
+      (goto-char (if (= init-point end)
+                    new-end
+                  (min init-point (- new-end 1)))))))
+
+(defun notmuch-pick-tag-update-display (&optional tag-changes)
+  "Update display for TAG-CHANGES to current message.
+
+Does NOT change the database."
+  (let* ((current-tags (notmuch-pick-get-tags))
+        (new-tags (notmuch-update-tags current-tags tag-changes)))
+    (unless (equal current-tags new-tags)
+      (notmuch-pick-set-tags new-tags)
+      (notmuch-pick-refresh-result))))
+
+(defun notmuch-pick-tag (&optional tag-changes)
+  "Change tags for the current message"
+  (interactive)
+  (setq tag-changes (funcall 'notmuch-tag (notmuch-pick-get-message-id) tag-changes))
+  (notmuch-pick-tag-update-display tag-changes))
+
+(defun notmuch-pick-add-tag ()
+  "Same as `notmuch-pick-tag' but sets initial input to '+'."
+  (interactive)
+  (notmuch-pick-tag "+"))
+
+(defun notmuch-pick-remove-tag ()
+  "Same as `notmuch-pick-tag' but sets initial input to '-'."
+  (interactive)
+  (notmuch-pick-tag "-"))
+
+;; The next two functions close the message window before searching or
+;; picking but they do so after the user has entered the query (in
+;; case the user was basing the query on something in the message
+;; window).
+
+(defun notmuch-pick-to-search ()
+  "Run \"notmuch search\" with the given `query' and display results."
+  (interactive)
+  (let ((query (notmuch-read-query "Notmuch search: ")))
+    (notmuch-pick-close-message-window)
+    (notmuch-search query)))
+
+(defun notmuch-pick-to-pick ()
+  "Run a query and display results in experimental notmuch-pick mode"
+  (interactive)
+  (let ((query (notmuch-read-query "Notmuch pick: ")))
+    (notmuch-pick-close-message-window)
+    (notmuch-pick query)))
+
+;; This function should be in notmuch-hello.el but we are trying to
+;; minimise impact on the rest of the codebase.
+(defun notmuch-pick-from-hello (&optional search)
+  "Run a query and display results in experimental notmuch-pick mode"
+  (interactive)
+  (unless (null search)
+    (setq search (notmuch-hello-trim search))
+    (let ((history-delete-duplicates t))
+      (add-to-history 'notmuch-search-history search)))
+  (notmuch-pick search))
+
+;; This function should be in notmuch-show.el but be we trying to
+;; minimise impact on the rest of the codebase.
+(defun notmuch-pick-from-show-current-query ()
+  "Call notmuch pick with the current query"
+  (interactive)
+  (notmuch-pick notmuch-show-thread-id notmuch-show-query-context))
+
+;; This function should be in notmuch.el but be we trying to minimise
+;; impact on the rest of the codebase.
+(defun notmuch-pick-from-search-current-query ()
+  "Call notmuch pick with the current query"
+  (interactive)
+  (notmuch-pick notmuch-search-query-string))
+
+;; This function should be in notmuch.el but be we trying to minimise
+;; impact on the rest of the codebase.
+(defun notmuch-pick-from-search-thread ()
+  "Show the selected thread with notmuch-pick"
+  (interactive)
+  (notmuch-pick (notmuch-search-find-thread-id)
+                notmuch-search-query-string
+                (notmuch-prettify-subject (notmuch-search-find-subject)))
+  (notmuch-pick-show-match-message-with-wait))
+
+(defun notmuch-pick-message-window-kill-hook ()
+  (let ((buffer (current-buffer)))
+    (when (and (window-live-p notmuch-pick-message-window)
+              (eq (window-buffer notmuch-pick-message-window) buffer))
+      ;; We do not want an error if this is the sole window in the
+      ;; frame and I do not know how to test for that in emacs pre
+      ;; 24. Hence we just ignore-errors.
+      (ignore-errors
+       (delete-window notmuch-pick-message-window)))))
+
+(defun notmuch-pick-show-message ()
+  "Show the current message (in split-pane)."
+  (interactive)
+  (let ((id (notmuch-pick-get-message-id))
+       (inhibit-read-only t)
+       buffer)
+    (when id
+      ;; We close and reopen the window to kill off un-needed buffers
+      ;; this might cause flickering but seems ok.
+      (notmuch-pick-close-message-window)
+      (setq notmuch-pick-message-window
+           (split-window-vertically (/ (window-height) 4)))
+      (with-selected-window notmuch-pick-message-window
+       ;; Since we are only displaying one message do not indent.
+       (let ((notmuch-show-indent-messages-width 0))
+         (setq current-prefix-arg '(4))
+         (setq buffer (notmuch-show id nil nil nil))))
+      ;; We need the `let' as notmuch-pick-message-window is buffer local.
+      (let ((window notmuch-pick-message-window))
+       (with-current-buffer buffer
+         (setq notmuch-pick-message-window window)
+         (add-hook 'kill-buffer-hook 'notmuch-pick-message-window-kill-hook)))
+      (notmuch-pick-tag-update-display (list "-unread"))
+      (setq notmuch-pick-message-buffer buffer))))
+
+(defun notmuch-pick-show-message-out ()
+  "Show the current message (in whole window)."
+  (interactive)
+  (let ((id (notmuch-pick-get-message-id))
+       (inhibit-read-only t)
+       buffer)
+    (when id
+      ;; We close the window to kill off un-needed buffers.
+      (notmuch-pick-close-message-window)
+      (notmuch-show id nil nil nil))))
+
+(defun notmuch-pick-scroll-message-window ()
+  "Scroll the message window (if it exists)"
+  (interactive)
+  (when (window-live-p notmuch-pick-message-window)
+    (with-selected-window notmuch-pick-message-window
+      (if (pos-visible-in-window-p (point-max))
+         t
+       (scroll-up)))))
+
+(defun notmuch-pick-scroll-message-window-back ()
+  "Scroll the message window back(if it exists)"
+  (interactive)
+  (when (window-live-p notmuch-pick-message-window)
+    (with-selected-window notmuch-pick-message-window
+      (if (pos-visible-in-window-p (point-min))
+         t
+       (scroll-down)))))
+
+(defun notmuch-pick-scroll-or-next ()
+  "Scroll the message window. If it at end go to next message."
+  (interactive)
+  (when (notmuch-pick-scroll-message-window)
+    (notmuch-pick-next-matching-message)))
+
+(defun notmuch-pick-quit ()
+  "Close the split view or exit pick."
+  (interactive)
+  (unless (notmuch-pick-close-message-window)
+    (kill-buffer (current-buffer))))
+
+(defun notmuch-pick-close-message-window ()
+  "Close the message-window. Return t if close succeeds."
+  (interactive)
+  (when (and (window-live-p notmuch-pick-message-window)
+            (eq (window-buffer notmuch-pick-message-window) notmuch-pick-message-buffer))
+    (delete-window notmuch-pick-message-window)
+    (unless (get-buffer-window-list notmuch-pick-message-buffer)
+      (kill-buffer notmuch-pick-message-buffer))
+    t))
+
+(defun notmuch-pick-archive-message (&optional unarchive)
+  "Archive the current message.
+
+Archive the current message by applying the tag changes in
+`notmuch-archive-tags' to it (remove the \"inbox\" tag by
+default). If a prefix argument is given, the message will be
+\"unarchived\", i.e. the tag changes in `notmuch-archive-tags'
+will be reversed."
+  (interactive "P")
+  (when notmuch-archive-tags
+    (apply 'notmuch-pick-tag
+          (notmuch-tag-change-list notmuch-archive-tags unarchive))))
+
+(defun notmuch-pick-archive-message-then-next (&optional unarchive)
+  "Archive the current message and move to next matching message."
+  (interactive "P")
+  (notmuch-pick-archive-message unarchive)
+  (notmuch-pick-next-matching-message))
+
+(defun notmuch-pick-next-message ()
+  "Move to next message."
+  (interactive)
+  (forward-line)
+  (when (window-live-p notmuch-pick-message-window)
+    (notmuch-pick-show-message)))
+
+(defun notmuch-pick-prev-message ()
+  "Move to previous message."
+  (interactive)
+  (forward-line -1)
+  (when (window-live-p notmuch-pick-message-window)
+    (notmuch-pick-show-message)))
+
+(defun notmuch-pick-prev-matching-message ()
+  "Move to previous matching message."
+  (interactive)
+  (forward-line -1)
+  (while (and (not (bobp)) (not (notmuch-pick-get-match)))
+    (forward-line -1))
+  (when (window-live-p notmuch-pick-message-window)
+    (notmuch-pick-show-message)))
+
+(defun notmuch-pick-next-matching-message ()
+  "Move to next matching message."
+  (interactive)
+  (forward-line)
+  (while (and (not (eobp)) (not (notmuch-pick-get-match)))
+    (forward-line))
+  (when (window-live-p notmuch-pick-message-window)
+    (notmuch-pick-show-message)))
+
+(defun notmuch-pick-show-match-message-with-wait ()
+  "Show the first matching message but wait for it to appear or search to finish."
+  (interactive)
+  (unless (notmuch-pick-get-match)
+    (notmuch-pick-next-matching-message))
+  (while (and (not (notmuch-pick-get-match))
+             (get-buffer-process (current-buffer)))
+    (message "waiting for message")
+    (sit-for 0.1)
+    (goto-char (point-min))
+    (unless (notmuch-pick-get-match)
+      (notmuch-pick-next-matching-message)))
+  (message nil)
+  (when (notmuch-pick-get-match)
+    (notmuch-pick-show-message)))
+
+(defun notmuch-pick-refresh-view ()
+  "Refresh view."
+  (interactive)
+  (let ((inhibit-read-only t)
+       (basic-query notmuch-pick-basic-query)
+       (query-context notmuch-pick-query-context)
+       (buffer-name notmuch-pick-buffer-name))
+    (erase-buffer)
+    (notmuch-pick-worker basic-query query-context (get-buffer buffer-name))))
+
+(defmacro with-current-notmuch-pick-message (&rest body)
+  "Evaluate body with current buffer set to the text of current message"
+  `(save-excursion
+     (let ((id (notmuch-pick-get-message-id)))
+       (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
+         (with-current-buffer buf
+           (call-process notmuch-command nil t nil "show" "--format=raw" id)
+           ,@body)
+        (kill-buffer buf)))))
+
+(defun notmuch-pick-new-mail (&optional prompt-for-sender)
+  "Compose new mail."
+  (interactive "P")
+  (notmuch-pick-close-message-window)
+  (notmuch-mua-new-mail prompt-for-sender ))
+
+(defun notmuch-pick-forward-message (&optional prompt-for-sender)
+  "Forward the current message."
+  (interactive "P")
+  (notmuch-pick-close-message-window)
+  (with-current-notmuch-pick-message
+   (notmuch-mua-new-forward-message prompt-for-sender)))
+
+(defun notmuch-pick-reply (&optional prompt-for-sender)
+  "Reply to the sender and all recipients of the current message."
+  (interactive "P")
+  (notmuch-pick-close-message-window)
+  (notmuch-mua-new-reply (notmuch-pick-get-message-id) prompt-for-sender t))
+
+(defun notmuch-pick-reply-sender (&optional prompt-for-sender)
+  "Reply to the sender of the current message."
+  (interactive "P")
+  (notmuch-pick-close-message-window)
+  (notmuch-mua-new-reply (notmuch-pick-get-message-id) prompt-for-sender nil))
+
+;; Shamelessly stolen from notmuch-show.el: maybe should be unified.
+(defun notmuch-pick-pipe-message (command)
+  "Pipe the contents of the current message to the given command.
+
+The given command will be executed with the raw contents of the
+current email message as stdin. Anything printed by the command
+to stdout or stderr will appear in the *notmuch-pipe* buffer.
+
+When invoked with a prefix argument, the command will receive all
+open messages in the current thread (formatted as an mbox) rather
+than only the current message."
+  (interactive "sPipe message to command: ")
+  (let ((shell-command
+        (concat notmuch-command " show --format=raw "
+                (shell-quote-argument (notmuch-pick-get-message-id)) " | " command))
+        (buf (get-buffer-create (concat "*notmuch-pipe*"))))
+    (with-current-buffer buf
+      (setq buffer-read-only nil)
+      (erase-buffer)
+      (let ((exit-code (call-process-shell-command shell-command nil buf)))
+       (goto-char (point-max))
+       (set-buffer-modified-p nil)
+       (setq buffer-read-only t)
+       (unless (zerop exit-code)
+         (switch-to-buffer-other-window buf)
+         (message (format "Command '%s' exited abnormally with code %d"
+                          shell-command exit-code)))))))
+
+(defun notmuch-pick-clean-address (address)
+  "Try to clean a single email ADDRESS for display. Return
+AUTHOR_NAME if present, otherwise return AUTHOR_EMAIL. Return
+unchanged ADDRESS if parsing fails."
+  (let* ((clean-address (notmuch-clean-address address))
+        (p-address (car clean-address))
+        (p-name (cdr clean-address)))
+
+    ;; If we have a name return that otherwise return the address.
+    (or p-name p-address)))
+
+(defun notmuch-pick-insert-field (field format-string msg)
+  (let* ((headers (plist-get msg :headers))
+       (match (plist-get msg :match)))
+    (cond
+     ((string-equal field "date")
+      (let ((face (if match
+                     'notmuch-pick-match-date-face
+                   'notmuch-pick-no-match-date-face)))
+       (insert (propertize (format format-string (plist-get msg :date_relative))
+                           'face face))))
+
+     ((string-equal field "subject")
+      (let ((tree-status (plist-get msg :tree-status))
+           (bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))
+           (face (if match
+                     'notmuch-pick-match-subject-face
+                   'notmuch-pick-no-match-subject-face)))
+       (insert (propertize (format format-string
+                                   (concat
+                                    (mapconcat #'identity (reverse tree-status) "")
+                                    (if (string= notmuch-pick-previous-subject bare-subject)
+                                        " ..."
+                                      bare-subject)))
+                           'face face))
+       (setq notmuch-pick-previous-subject bare-subject)))
+
+     ((string-equal field "authors")
+      (let ((author (notmuch-pick-clean-address (plist-get headers :From)))
+           (len (length (format format-string "")))
+           (face (if match
+                     'notmuch-pick-match-author-face
+                   'notmuch-pick-no-match-author-face)))
+       (when (> (length author) len)
+         (setq author (substring author 0 len)))
+       (insert (propertize (format format-string author)
+                           'face face))))
+
+     ((string-equal field "tags")
+      (let ((tags (plist-get msg :tags))
+           (face (if match
+                         'notmuch-pick-match-tag-face
+                       'notmuch-pick-no-match-tag-face)))
+       (when tags
+         (insert (propertize (format format-string
+                                     (mapconcat #'identity tags ", "))
+                             'face face))))))))
+
+(defun notmuch-pick-insert-msg (msg)
+  "Insert the message MSG according to notmuch-pick-result-format"
+  (dolist (spec notmuch-pick-result-format)
+    (notmuch-pick-insert-field (car spec) (cdr spec) msg))
+  (notmuch-pick-set-message-properties msg)
+  (insert "\n"))
+
+(defun notmuch-pick-insert-tree (tree depth tree-status first last)
+  "Insert the message tree TREE at depth DEPTH in the current thread."
+  (let ((msg (car tree))
+       (replies (cadr tree)))
+
+      (cond
+       ((and (< 0 depth) (not last))
+       (push "├" tree-status))
+       ((and (< 0 depth) last)
+       (push "╰" tree-status))
+       ((and (eq 0 depth) first last)
+;;       (push "─" tree-status)) choice between this and next line is matter of taste.
+       (push " " tree-status))
+       ((and (eq 0 depth) first (not last))
+         (push "┬" tree-status))
+       ((and (eq 0 depth) (not first) last)
+       (push "╰" tree-status))
+       ((and (eq 0 depth) (not first) (not last))
+       (push "├" tree-status)))
+
+      (push (concat (if replies "┬" "─") "►") tree-status)
+      (notmuch-pick-insert-msg (plist-put msg :tree-status tree-status))
+      (pop tree-status)
+      (pop tree-status)
+
+      (if last
+         (push " " tree-status)
+       (push "│" tree-status))
+
+    (notmuch-pick-insert-thread replies (1+ depth) tree-status)))
+
+(defun notmuch-pick-insert-thread (thread depth tree-status)
+  "Insert the thread THREAD at depth DEPTH >= 1 in the current forest."
+  (let ((n (length thread)))
+    (loop for tree in thread
+         for count from 1 to n
+
+         do (notmuch-pick-insert-tree tree depth tree-status (eq count 1) (eq count n)))))
+
+(defun notmuch-pick-insert-forest-thread (forest-thread)
+  (save-excursion
+    (goto-char (point-max))
+    (let (tree-status)
+      ;; Reset at the start of each main thread.
+      (setq notmuch-pick-previous-subject nil)
+      (notmuch-pick-insert-thread forest-thread 0 tree-status))))
+
+(defun notmuch-pick-insert-forest (forest)
+  (mapc 'notmuch-pick-insert-forest-thread forest))
+
+(defun notmuch-pick-mode ()
+  "Major mode displaying messages (as opposed to threads) of of a notmuch search.
+
+This buffer contains the results of a \"notmuch pick\" of your
+email archives. Each line in the buffer represents a single
+message giving the relative date, the author, subject, and any
+tags.
+
+Pressing \\[notmuch-pick-show-message] on any line displays that message.
+
+Complete list of currently available key bindings:
+
+\\{notmuch-pick-mode-map}"
+
+  (interactive)
+  (kill-all-local-variables)
+  (use-local-map notmuch-pick-mode-map)
+  (setq major-mode 'notmuch-pick-mode
+       mode-name "notmuch-pick")
+  (hl-line-mode 1)
+  (setq buffer-read-only t
+       truncate-lines t))
+
+(defun notmuch-pick-process-sentinel (proc msg)
+  "Add a message to let user know when \"notmuch pick\" exits"
+  (let ((buffer (process-buffer proc))
+       (status (process-status proc))
+       (exit-status (process-exit-status proc))
+       (never-found-target-thread nil))
+    (when (memq status '(exit signal))
+        (kill-buffer (process-get proc 'parse-buf))
+       (if (buffer-live-p buffer)
+           (with-current-buffer buffer
+             (save-excursion
+               (let ((inhibit-read-only t)
+                     (atbob (bobp)))
+                 (goto-char (point-max))
+                 (if (eq status 'signal)
+                     (insert "Incomplete search results (pick process was killed).\n"))
+                 (when (eq status 'exit)
+                   (insert "End of search results.")
+                   (unless (= exit-status 0)
+                     (insert (format " (process returned %d)" exit-status)))
+                   (insert "\n")))))))))
+
+
+(defun notmuch-pick-show-error (string &rest objects)
+  (save-excursion
+    (goto-char (point-max))
+    (insert "Error: Unexpected output from notmuch search:\n")
+    (insert (apply #'format string objects))
+    (insert "\n")))
+
+
+(defun notmuch-pick-process-filter (proc string)
+  "Process and filter the output of \"notmuch show\" (for pick)"
+  (let ((results-buf (process-buffer proc))
+        (parse-buf (process-get proc 'parse-buf))
+        (inhibit-read-only t)
+        done)
+    (if (not (buffer-live-p results-buf))
+        (delete-process proc)
+      (with-current-buffer parse-buf
+        ;; Insert new data
+        (save-excursion
+          (goto-char (point-max))
+          (insert string))
+       (notmuch-json-parse-partial-list 'notmuch-pick-insert-forest-thread
+                                        'notmuch-pick-show-error
+                                        results-buf)))))
+
+(defun notmuch-pick-worker (basic-query &optional query-context buffer)
+  (interactive)
+  (notmuch-pick-mode)
+  (setq notmuch-pick-basic-query basic-query)
+  (setq notmuch-pick-query-context query-context)
+  (setq notmuch-pick-buffer-name (buffer-name buffer))
+
+  (erase-buffer)
+  (goto-char (point-min))
+  (let* ((search-args (concat basic-query
+                      (if query-context (concat " and (" query-context ")"))
+                      ))
+        (message-arg "--entire-thread"))
+    (if (equal (car (process-lines notmuch-command "count" search-args)) "0")
+       (setq search-args basic-query))
+    (if notmuch-pick-asynchronous-parser
+       (let ((proc (start-process
+                    "notmuch-pick" buffer
+                    notmuch-command "show" "--body=false" "--format=json"
+                    message-arg search-args))
+             ;; Use a scratch buffer to accumulate partial output.
+              ;; This buffer will be killed by the sentinel, which
+              ;; should be called no matter how the process dies.
+              (parse-buf (generate-new-buffer " *notmuch pick parse*")))
+          (process-put proc 'parse-buf parse-buf)
+         (set-process-sentinel proc 'notmuch-pick-process-sentinel)
+         (set-process-filter proc 'notmuch-pick-process-filter)
+         (set-process-query-on-exit-flag proc nil))
+      (progn
+       (notmuch-pick-insert-forest
+        (notmuch-query-get-threads
+         (list "--body=false" message-arg search-args)))
+       (save-excursion
+         (goto-char (point-max))
+         (insert "End of search results.\n"))))))
+
+
+(defun notmuch-pick (&optional query query-context buffer-name show-first-match)
+  "Run notmuch pick with the given `query' and display the results"
+  (interactive "sNotmuch pick: ")
+  (if (null query)
+      (setq query (notmuch-read-query "Notmuch pick: ")))
+  (let ((buffer (get-buffer-create (generate-new-buffer-name
+                                   (or buffer-name
+                                       (concat "*notmuch-pick-" query "*")))))
+       (inhibit-read-only t))
+
+    (switch-to-buffer buffer)
+    ;; Don't track undo information for this buffer
+    (set 'buffer-undo-list t)
+
+    (notmuch-pick-worker query query-context buffer)
+
+    (setq truncate-lines t)
+    (when show-first-match
+      (notmuch-pick-show-match-message-with-wait))))
+
+
+;; Set up key bindings from the rest of notmuch.
+(define-key 'notmuch-search-mode-map "z" 'notmuch-pick)
+(define-key 'notmuch-search-mode-map "Z" 'notmuch-pick-from-search-current-query)
+(define-key 'notmuch-search-mode-map (kbd "M-RET") 'notmuch-pick-from-search-thread)
+(define-key 'notmuch-hello-mode-map "z" 'notmuch-pick-from-hello)
+(define-key 'notmuch-show-mode-map "z" 'notmuch-pick)
+(define-key 'notmuch-show-mode-map "Z" 'notmuch-pick-from-show-current-query)
+(notmuch-pick-setup-show-out)
+(message "Initialised notmuch-pick")
+
+(provide 'notmuch-pick)
diff --git a/contrib/notmuch-pick/run-tests.sh b/contrib/notmuch-pick/run-tests.sh
new file mode 100755 (executable)
index 0000000..7ddc9cc
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+
+set -eu
+
+fail() {
+    echo ERROR $1
+    exit 1
+}
+
+TESTS="emacs-pick emacs-pick-sync"
+TESTFILES="$TESTS pick.expected-output"
+
+export PICK_DIR="`cd \`dirname "$0"\` && pwd`"
+PICK_TEST_DIR="$PICK_DIR/test"
+
+
+for f in $TESTFILES
+do
+    test -f "$PICK_TEST_DIR/$f" || test -d "$PICK_TEST_DIR/$f" || fail "$PICK_TEST_DIR/$f does not exist"
+done
+
+cd "$PICK_DIR/../../test"
+
+test -x ../notmuch || fail "`cd .. && pwd`/notmuch has not been built"
+
+for f in $TESTFILES
+do
+    if test -f "$f"
+    then
+       fail "$f exists"
+    fi
+done
+
+trap "rm -f $TESTFILES" 0
+
+for f in $TESTFILES
+do
+    ln -s "$PICK_TEST_DIR/$f" .
+done
+
+#don't exec -- traps would not run.
+for f in $TESTS
+do
+    echo $f
+    ./$f
+done
diff --git a/contrib/notmuch-pick/test/emacs-pick b/contrib/notmuch-pick/test/emacs-pick
new file mode 100755 (executable)
index 0000000..eed5f02
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+test_description="emacs pick interface"
+. test-lib.sh
+
+EXPECTED=$TEST_DIRECTORY/pick.expected-output
+
+add_email_corpus
+test_begin_subtest "Do we have emacs"
+test_emacs '(insert "hello\n")
+           (test-output)'
+cat <<EOF >EXPECTED
+hello
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "Basic notmuch-pick view in emacs"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+           (require (quote notmuch-pick))
+           (notmuch-pick "tag:inbox")
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-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 (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-pick-from-search-current-query)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-tag-inbox
+
+test_begin_subtest "Pick of a single thread (from search)"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-pick-from-search-thread)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
+
+test_begin_subtest "Pick of a single thread (from show)"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-search-show-thread)
+           (notmuch-pick-from-show-current-query)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
+
+test_begin_subtest "Message window of pick"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-search-next-thread)
+           (notmuch-pick-from-search-thread)
+           (notmuch-test-wait)
+           (select-window notmuch-pick-message-window)
+           (test-output)
+           (delete-other-windows)'
+cp OUTPUT /tmp/mjwout
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-show-window
+
+test_done
diff --git a/contrib/notmuch-pick/test/emacs-pick-sync b/contrib/notmuch-pick/test/emacs-pick-sync
new file mode 100755 (executable)
index 0000000..a7da0ff
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+
+test_description="emacs pick interface (sync parser)"
+. test-lib.sh
+
+EXPECTED=$TEST_DIRECTORY/pick.expected-output
+
+add_email_corpus
+test_begin_subtest "Do we have emacs"
+test_emacs '(insert "hello\n")
+           (test-output)'
+cat <<EOF >EXPECTED
+hello
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "Basic notmuch-pick view in emacs"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+           (require (quote notmuch-pick))
+           (setq notmuch-pick-asynchronous-parser nil)
+           (notmuch-pick "tag:inbox")
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-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 (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-pick-from-search-current-query)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-tag-inbox
+
+test_begin_subtest "Pick of a single thread (from search)"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-pick-from-search-thread)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
+
+test_begin_subtest "Pick of a single thread (from show)"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-search-show-thread)
+           (notmuch-pick-from-show-current-query)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
+
+test_done
diff --git a/contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-show-window b/contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-show-window
new file mode 100644 (file)
index 0000000..e16792b
--- /dev/null
@@ -0,0 +1,40 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+Subject: [notmuch] Working with Maildir storage?
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 14:00:54 -0500
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ text/plain ]
+I saw the LWN article and decided to take a look at notmuch.  I'm
+currently using mutt and mairix to index and read a collection of
+Maildir mail folders (around 40,000 messages total).
+
+notmuch indexed the messages without complaint, but my attempt at
+searching bombed out. Running, for example:
+
+  notmuch search storage
+
+Resulted in 4604 lines of errors along the lines of:
+
+  Error opening
+  /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+  Too many open files
+
+I'm curious if this is expected behavior (i.e., notmuch does not work
+with Maildir) or if something else is going on.
+
+Cheers,
+
+[ 4-line signature. Click/Enter to show. ]
+-- 
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
diff --git a/contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-single-thread b/contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-single-thread
new file mode 100644 (file)
index 0000000..c9e5ef8
--- /dev/null
@@ -0,0 +1,6 @@
+  2009-11-17  Mikhail Gusarov       ┬►[notmuch] [PATCH 1/2] Close message file after parsing message       headers (inbox)
+  2009-11-17  Mikhail Gusarov       ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++   file with gcc 4.4 (inbox, unread)
+  2009-11-17  Carl Worth            ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+  2009-11-17  Keith Packard          ╰┬► ...                                              (inbox, unread)
+  2009-11-18  Carl Worth              ╰─► ...                                             (inbox, unread)
+End of search results.
diff --git a/contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-tag-inbox b/contrib/notmuch-pick/test/pick.expected-output/notmuch-pick-tag-inbox
new file mode 100644 (file)
index 0000000..484141e
--- /dev/null
@@ -0,0 +1,53 @@
+  2010-12-29  François Boulogne     ─►[aur-general] Guidelines: cp, mkdir vs install      (inbox, unread)
+  2010-12-16  Olivier Berger        ─►Essai accentué                                      (inbox, unread)
+  2009-11-18  Chris Wilson          ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox, unread)
+  2009-11-18  Alex Botero-Lowry     ┬►[notmuch] [PATCH] Error out if no query is supplied to search        instead of going into an infinite loop (attachment, inbox, unread)
+  2009-11-18  Carl Worth            ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox, unread)
+  2009-11-17  Ingmar Vanhassel      ┬►[notmuch] [PATCH] Typsos                            (inbox, unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox, unread)
+  2009-11-17  Adrian Perez de Cast  ┬►[notmuch] Introducing myself                        (inbox, signed, unread)
+  2009-11-18  Keith Packard         ├─► ...                                               (inbox, unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox, unread)
+  2009-11-17  Israel Herraiz        ┬►[notmuch] New to the list                           (inbox, unread)
+  2009-11-18  Keith Packard         ├─► ...                                               (inbox, unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox, unread)
+  2009-11-17  Jan Janak             ┬►[notmuch] What a great idea!                        (inbox, unread)
+  2009-11-17  Jan Janak             ├─► ...                                               (inbox, unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox, unread)
+  2009-11-17  Jan Janak             ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox, unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox, unread)
+  2009-11-17  Aron Griffis          ┬►[notmuch] archive                                   (inbox, unread)
+  2009-11-18  Keith Packard         ╰┬► ...                                               (inbox, unread)
+  2009-11-18  Carl Worth             ╰─► ...                                              (inbox, unread)
+  2009-11-17  Keith Packard         ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove    inbox (and unread) tags (inbox, unread)
+  2009-11-18  Carl Worth            ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
+  2009-11-17  Lars Kellogg-Stedman  ┬►[notmuch] Working with Maildir storage?             (inbox, signed, unread)
+  2009-11-17  Mikhail Gusarov       ├┬► ...                                               (inbox, signed, unread)
+  2009-11-17  Lars Kellogg-Stedman  │╰┬► ...                                              (inbox, signed, unread)
+  2009-11-17  Mikhail Gusarov       │ ├─► ...                                             (inbox, unread)
+  2009-11-17  Keith Packard         │ ╰┬► ...                                             (inbox, unread)
+  2009-11-18  Lars Kellogg-Stedman  │  ╰─► ...                                            (inbox, signed, unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox, unread)
+  2009-11-17  Mikhail Gusarov       ┬►[notmuch] [PATCH 1/2] Close message file after parsing message       headers (inbox, unread)
+  2009-11-17  Mikhail Gusarov       ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++   file with gcc 4.4 (inbox, unread)
+  2009-11-17  Carl Worth            ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+  2009-11-17  Keith Packard          ╰┬► ...                                              (inbox, unread)
+  2009-11-18  Carl Worth              ╰─► ...                                             (inbox, unread)
+  2009-11-18  Keith Packard         ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that    highlights URLs and uses word-wrap (inbox, unread)
+  2009-11-18  Alexander Botero-Low  ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
+  2009-11-18  Alexander Botero-Low  ─►[notmuch] request for pull                          (inbox, unread)
+  2009-11-18  Jjgod Jiang           ┬►[notmuch] Mac OS X/Darwin compatibility issues      (inbox, unread)
+  2009-11-18  Alexander Botero-Low  ╰┬► ...                                               (inbox, unread)
+  2009-11-18  Jjgod Jiang            ╰┬► ...                                              (inbox, unread)
+  2009-11-18  Alexander Botero-Low    ╰─► ...                                             (inbox, unread)
+  2009-11-18  Rolland Santimano     ─►[notmuch] Link to mailing list archives ?           (inbox, unread)
+  2009-11-18  Jan Janak             ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool       subdirectories into tags (inbox, unread)
+  2009-11-18  Stewart Smith         ─►[notmuch] [PATCH] count_files: sort directory in inode order before  statting (inbox, unread)
+  2009-11-18  Stewart Smith         ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox, unread)
+  2009-11-18  Stewart Smith         ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++     libs. (inbox, unread)
+  2009-11-18  Lars Kellogg-Stedman  ┬►[notmuch] "notmuch help" outputs to stderr?         (attachment, inbox, signed, unread)
+  2009-11-18  Lars Kellogg-Stedman  ╰─► ...                                               (attachment, inbox, signed, unread)
+  2009-11-17  Mikhail Gusarov       ─►[notmuch] [PATCH] Handle rename of message file     (inbox, unread)
+  2009-11-17  Alex Botero-Lowry     ┬►[notmuch] preliminary FreeBSD support               (attachment, inbox, unread)
+  2009-11-17  Carl Worth            ╰─► ...                                               (inbox, unread)
+End of search results.
diff --git a/crypto.c b/crypto.c
new file mode 100644 (file)
index 0000000..fbe5aeb
--- /dev/null
+++ b/crypto.c
@@ -0,0 +1,71 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2012 Jameson Rollins
+ *
+ * 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/ .
+ *
+ * Authors: Jameson Rollins <jrollins@finestructure.net>
+ */
+
+#include "notmuch-client.h"
+
+/* for the specified protocol return the context pointer (initializing
+ * if needed) */
+notmuch_crypto_context_t *
+notmuch_crypto_get_context (notmuch_crypto_t *crypto, const char *protocol)
+{
+    notmuch_crypto_context_t *cryptoctx = NULL;
+
+    /* As per RFC 1847 section 2.1: "the [protocol] value token is
+     * comprised of the type and sub-type tokens of the Content-Type".
+     * As per RFC 1521 section 2: "Content-Type values, subtypes, and
+     * parameter names as defined in this document are
+     * case-insensitive."  Thus, we use strcasecmp for the protocol.
+     */
+    if ((strcasecmp (protocol, "application/pgp-signature") == 0)
+       || (strcasecmp (protocol, "application/pgp-encrypted") == 0)) {
+       if (!crypto->gpgctx) {
+#ifdef GMIME_ATLEAST_26
+           /* TODO: GMimePasswordRequestFunc */
+           crypto->gpgctx = g_mime_gpg_context_new (NULL, "gpg");
+#else
+           GMimeSession* session = g_object_new (g_mime_session_get_type(), NULL);
+           crypto->gpgctx = g_mime_gpg_context_new (session, "gpg");
+           g_object_unref (session);
+#endif
+           if (crypto->gpgctx) {
+               g_mime_gpg_context_set_always_trust ((GMimeGpgContext*) crypto->gpgctx, FALSE);
+           } else {
+               fprintf (stderr, "Failed to construct gpg context.\n");
+           }
+       }
+       cryptoctx = crypto->gpgctx;
+
+    } else {
+       fprintf (stderr, "Unknown or unsupported cryptographic protocol.\n");
+    }
+
+    return cryptoctx;
+}
+
+int
+notmuch_crypto_cleanup (notmuch_crypto_t *crypto)
+{
+    if (crypto->gpgctx) {
+       g_object_unref (crypto->gpgctx);
+       crypto->gpgctx = NULL;
+    }
+
+    return 0;
+}
index 69477542e843290f5e3042d57f5ec1b9c98df820..907d790700b17acc172cfd1a517f20e5e24d262c 100644 (file)
@@ -1,7 +1,20 @@
+notmuch (0.14-1) unstable; urgency=low
+
+  There is an incompatible change in option syntax for dump and restore
+  in this release. Please update your scripts.
+
+  From upstream NEWS:
+
+  The deprecated positional output file argument to notmuch dump has
+  been replaced with an --output option. The input file positional
+  argument for restore has been replaced with an --input option for
+  consistency with dump.
+
+ -- David Bremner <bremner@debian.org>  Sun, 05 Aug 2012 11:52:49 -0300
+
 notmuch (0.6~238) unstable; urgency=low
 
-  The emacs user interface to notmuch is now contained in a seperate
+  The emacs user interface to notmuch is now contained in a separate
   package called notmuch-emacs.
 
  -- David Bremner <bremner@debian.org>  Mon, 20 Jun 2011 23:57:55 -0300
-
index ad05e812312bcdad44a2c214f743e15808c6efc0..68132e3293ffab552d805b10dce84e2f6dd35bf9 100644 (file)
@@ -1,3 +1,67 @@
+notmuch (0.15.1-1) experimental; urgency=low
+
+  * Upstream bug fix release: set default TERM for running tests.
+  * Re-enable build time self-tests.
+
+ -- David Bremner <bremner@debian.org>  Thu, 24 Jan 2013 07:19:45 -0400
+
+notmuch (0.15-2) experimental; urgency=low
+
+  * Disable tests until a proper fix for running tests without a
+    proper TERM value is developed (again).
+
+ -- David Bremner <bremner@debian.org>  Sun, 20 Jan 2013 18:36:16 -0400
+
+notmuch (0.15-1) experimental; urgency=low
+
+  * New upstream release.
+    - Date range search support
+    - Empty tag names and tags beginning with "-" are deprecated
+    - Support for single message mboxes is deprecated
+    - Fixed `notmuch new` to skip ignored broken symlinks
+    - New dump/restore format and tagging interface
+    - Emacs Interface
+      - Removal of the deprecated `notmuch-folders` variable
+      - Visibility of MIME parts can be toggled
+      - Emacs now buttonizes mid: links
+      - Improved text/calendar content handling
+      - Disabled coding conversions when reading
+      - Fixed errors with HTML email containing images in Emacs 24
+      - Fixed handling of tags with unusual characters in them
+      - Fixed buttonization of id: links without quote characters
+      - Automatic tag changes are now unified and customizable
+      - Support for stashing the thread id in show view
+      - New add-on tool: notmuch-pick
+
+ -- David Bremner <bremner@debian.org>  Fri, 18 Jan 2013 21:23:36 -0400
+
+notmuch (0.15~rc1-1) experimental; urgency=low
+
+  * New upstream release candidate.
+  * Change priority to optional (Closes: #687217).
+  * Remove Dm-Upload-Allowed field, as this is no longer used by
+    Debian.
+  * Add python3 bindings, thanks to Jakub Wilk (Closes: #683515).
+  * Bug fix: ".ical attachment problem", (Closes: #688747).
+
+ -- David Bremner <bremner@debian.org>  Wed, 16 Jan 2013 08:28:27 -0400
+
+notmuch (0.14-1) experimental; urgency=low
+
+  [ Stefano Zacchiroli ]
+  * notmuch-mutt: fix tag action invocation (Closes: #678012)
+  * Use notmuch-search-terms manpage in notmuch-mutt (Closes: #675073).
+
+  [ David Bremner ]
+  * Do a better job of cleaning up after configuration and testing
+    (Closes: #683505)
+  * Alternately depend on emacs24 instead of emacs23 (Closes: #677900).
+  * New upstream version
+    - incompatible changes to dump/restore syntax
+    - bug fixes for maildir synchronization
+
+ -- David Bremner <bremner@debian.org>  Tue, 21 Aug 2012 10:39:33 +0200
+
 notmuch (0.13.2-1~bpo60+1) squeeze-backports; urgency=low
 
   * Rebuild for squeeze-backports.
index 7f8f011eb73d6043d2e6db9d2c101195ae2801f2..ec635144f60048986bc560c5576355344005e6e7 100644 (file)
@@ -1 +1 @@
-7
+9
index 812430fb84b9359c97a794d76f779b61fb2c2d85..5bb0d051e2f1b44e7ea3b1016e44ac11326ae552 100644 (file)
@@ -1,27 +1,28 @@
 Source: notmuch
 Section: mail
-Priority: extra
+Priority: optional
 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~),
+ debhelper (>= 9),
  pkg-config,
  libxapian-dev,
  libgmime-2.6-dev (>= 2.6.7~) | libgmime-2.4-dev,
  libtalloc-dev,
  libz-dev,
  python-all (>= 2.6.6-3~),
- emacs23-nox | emacs23 (>=23~) | emacs23-lucid (>=23~),
+ python3-all (>= 3.1.2-7~),
+ emacs23-nox | emacs23 (>=23~) | emacs23-lucid (>=23~) |
+ emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~),
  gdb,
  dtach (>= 0.8)
 Standards-Version: 3.9.3
 Homepage: http://notmuchmail.org/
 Vcs-Git: git://notmuchmail.org/git/notmuch
 Vcs-Browser: http://git.notmuchmail.org/git/notmuch
-Dm-Upload-Allowed: yes
 
 Package: notmuch
 Architecture: any
@@ -39,6 +40,7 @@ Package: libnotmuch3
 Section: libs
 Architecture: any
 Depends: ${shlibs:Depends}, ${misc:Depends}
+Pre-Depends: ${misc:Pre-Depends}
 Description: thread-based email index, search and tagging (runtime)
  Notmuch is a system for indexing, searching, reading, and tagging
  large collections of email messages in maildir or mh format. It uses
@@ -74,13 +76,27 @@ Description: python interface to the notmuch mail search and index library
  This package provides a Python interface to the notmuch
  functionality, directly interfacing with a shared notmuch library.
 
+Package: python3-notmuch
+Architecture: all
+Section: python
+Depends: ${misc:Depends}, ${python3:Depends}, libnotmuch3
+Description: Python 3 interface to the notmuch mail search and index library
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides a Python 3 interface to the notmuch
+ functionality, directly interfacing with a shared notmuch library.
+
 Package: notmuch-emacs
 Architecture: all
 Section: mail
 Breaks: notmuch (<<0.6~254~)
 Replaces: notmuch (<<0.6~254~)
 Depends: ${misc:Depends}, notmuch (>= ${source:Version}),
- emacs23 (>= 23~) | emacs23-nox (>=23~) | emacs23-lucid (>=23~)
+ emacs23 (>= 23~) | emacs23-nox (>=23~) | emacs23-lucid (>=23~) |
+ emacs24 (>= 24~) | emacs24-nox (>=24~) | emacs24-lucid (>=24~)
 Description: thread-based email index, search and tagging (emacs interface)
  Notmuch is a system for indexing, searching, reading, and tagging
  large collections of email messages in maildir or mh format. It uses
@@ -109,8 +125,9 @@ Package: notmuch-mutt
 Architecture: all
 Depends: notmuch, libmail-box-perl, libmailtools-perl,
  libstring-shellquote-perl, libterm-readline-gnu-perl,
+ libfile-which-perl,
  ${misc:Depends}
-Recommends: mutt
+Recommends: mutt, fdupes
 Enhances: notmuch, mutt
 Description: thread-based email index, search and tagging (Mutt interface)
  notmuch-mutt provides integration among the Mutt mail user agent and
index 185dba4e32281492b9a594c175ee1666d5b0b720..96bbd63fe920e732114b28f47d515e953b138988 100644 (file)
@@ -1,2 +1,2 @@
 usr/include
-usr/lib/libnotmuch.so
+usr/lib/*/libnotmuch.so
index da4fc25ba27172574ebd0c05721c49bd05f82e5b..a513b4757f05eb3fbf9370fb319967084ef43c1d 100644 (file)
@@ -1 +1 @@
-usr/lib/libnotmuch.so.*
+usr/lib/*/libnotmuch.so.*
index 607c0659ca9a5a9c309a61a11e78ad65626a4b75..b2cc1360acc4cb648679bbcb2f59a031eb697fcc 100644 (file)
@@ -1 +1 @@
-usr/lib/python*
+usr/lib/python2*
diff --git a/debian/python3-notmuch.install b/debian/python3-notmuch.install
new file mode 100644 (file)
index 0000000..4606faa
--- /dev/null
@@ -0,0 +1 @@
+usr/lib/python3*
index 603b3ab281a2bdd27f2073d3a988e1c5838c1fa6..c4e3930da5fb931c4e1bd39e017884e33d8e9a53 100755 (executable)
@@ -1,7 +1,9 @@
 #!/usr/bin/make -f
 
+python3_all = py3versions -s | xargs -n1 | xargs -t -I {} env {}
+
 %:
-       dh --with python2 $@
+       dh $@ --with python2,python3
 
 override_dh_auto_configure:
        dh_auto_configure -- --emacslispdir=/usr/share/emacs/site-lisp/notmuch
@@ -9,13 +11,16 @@ override_dh_auto_configure:
 override_dh_auto_build:
        dh_auto_build
        dh_auto_build --sourcedirectory bindings/python
+       cd bindings/python && $(python3_all) setup.py build
        $(MAKE) -C contrib/notmuch-mutt
 
 override_dh_auto_clean:
        dh_auto_clean
        dh_auto_clean --sourcedirectory bindings/python
+       cd bindings/python && $(python3_all) setup.py clean -a
        $(MAKE) -C contrib/notmuch-mutt clean
 
 override_dh_auto_install:
        dh_auto_install
        dh_auto_install --sourcedirectory bindings/python
+       cd bindings/python && $(python3_all) setup.py install --install-layout=deb --root=$(CURDIR)/debian/tmp
index 094f71d1908f75b48f3d1ae77a4fbae9d260acb2..0792ba129e240eb1818b1cc2d98f1b16746964f0 100644 (file)
@@ -73,6 +73,12 @@ Naming
   struct has a tag, it should be the same as the typedef name, minus
   the trailing _t.
 
+CLI conventions
+---------------
+
+* Any changes to the JSON output format of search or show need an
+  accompanying change to devel/schemata.
+
 libnotmuch conventions
 ----------------------------------
 
index 7b750afa5cbde78b868f6ec111630aca8ac8da29..eb757af5da2ab3c75099189dfc6c0de020f107bb 100644 (file)
@@ -92,8 +92,6 @@ and email address in the From: line. We could also then easily support
 "notmuch compose --from <something>" to support getting at alternate
 email addresses.
 
-Fix the --format=json option to not imply --entire-thread.
-
 Implement "notmuch search --exclude-threads=<search-terms>" to allow
 for excluding muted threads, (and any other negative, thread-based
 filtering that the user wants to do).
diff --git a/devel/release-checks.sh b/devel/release-checks.sh
new file mode 100755 (executable)
index 0000000..e1d19f2
--- /dev/null
@@ -0,0 +1,209 @@
+#!/usr/bin/env bash
+
+set -eu
+#set -x # or enter bash -x ... on command line
+
+if [ x"${BASH_VERSION-}" = x ]
+then   echo
+       echo "Please execute this script using 'bash' interpreter"
+       echo
+       exit 1
+fi
+
+set -o posix
+set -o pipefail # bash feature
+
+# Avoid locale-specific differences in output of executed commands
+LANG=C LC_ALL=C; export LANG LC_ALL
+
+readonly PV_FILE='bindings/python/notmuch/version.py'
+
+# Using array here turned out to be unnecessarily complicated
+emsgs=''
+append_emsg ()
+{
+       emsgs="${emsgs:+$emsgs\n}  $1"
+}
+
+for f in ./version debian/changelog NEWS "$PV_FILE"
+do
+       if   [ ! -f "$f" ]; then append_emsg "File '$f' is missing"
+       elif [ ! -r "$f" ]; then append_emsg "File '$f' is unreadable"
+       elif [ ! -s "$f" ]; then append_emsg "File '$f' is empty"
+       fi
+done
+
+if [ -n "$emsgs" ]
+then
+       echo 'Release files problems; fix these and try again:'
+       echo -e "$emsgs"
+       exit 1
+fi
+
+if read VERSION
+then
+       if read rest
+       then    echo "'version' file contains more than one line"
+               exit 1
+       fi
+else
+       echo "Reading './version' file failed (suprisingly!)"
+       exit 1
+fi < ./version
+
+readonly VERSION
+
+verfail ()
+{
+       echo No.
+       echo "$@"
+       echo "Please follow the instructions in RELEASING to choose a version"
+       exit 1
+}
+
+echo -n "Checking that '$VERSION' is good with digits and periods... "
+case $VERSION in
+       *[^0-9.]*)
+               verfail "'$VERSION' contains other characters than digits and periods" ;;
+       .*)     verfail "'$VERSION' begins with a period" ;;
+       *.)     verfail "'$VERSION' ends with a period" ;;
+       *..*)   verfail "'$VERSION' contains two consecutive periods" ;;
+       *.*)    echo Yes. ;;
+       *)      verfail "'$VERSION' is a single number" ;;
+esac
+
+
+# In the rest of this file, tests collect list of errors to be fixed
+
+echo -n "Checking that this is Debian package for notmuch... "
+read deb_notmuch deb_version rest < debian/changelog
+if [ "$deb_notmuch" = 'notmuch' ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Package name '$deb_notmuch' is not 'notmuch' in debian/changelog"
+fi
+
+echo -n "Checking that Debian package version is $VERSION-1... "
+
+if [ "$deb_version" = "($VERSION-1)" ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Version '$deb_version' is not '($VERSION-1)' in debian/changelog"
+fi
+
+echo -n "Checking that python bindings version is $VERSION... "
+py_version=`python -c "execfile('$PV_FILE'); print __VERSION__"`
+if [ "$py_version" = "$VERSION" ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Version '$py_version' is not '$VERSION' in $PV_FILE"
+fi
+
+echo -n "Checking that this is Notmuch NEWS... "
+read news_notmuch news_version news_date < NEWS
+if [ "$news_notmuch" = "Notmuch" ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "First word '$news_notmuch' is not 'Notmuch' in NEWS file"
+fi
+
+echo -n "Checking that NEWS version is $VERSION... "
+if [ "$news_version" = "$VERSION" ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Version '$news_version' in NEWS file is not '$VERSION'"
+fi
+
+#eval `date '+year=%Y mon=%m day=%d'`
+today0utc=`date --date=0Z +%s` # gnu date feature
+
+echo -n "Checking that NEWS date is right... "
+case $news_date in
+ '('[2-9][0-9][0-9][0-9]-[01][0-9]-[0123][0-9]')')
+       newsdate0utc=`nd=${news_date#\\(}; date --date="${nd%)} 0Z" +%s`
+       ddiff=$((newsdate0utc - today0utc))
+       if [ $ddiff -lt -86400 ] # since beginning of yesterday...
+       then
+               echo No.
+               append_emsg "Date $news_date in NEWS file is too much in the past"
+       elif [ $ddiff -gt 172800 ] # up to end of tomorrow...
+       then
+               echo No.
+               append_emsg "Date $news_date in NEWS file is too much in the future"
+       else
+               echo Yes.
+       fi ;;
+ *)
+       echo No.
+       append_emsg "Date '$news_date' in NEWS file is not in format (yyyy-mm-dd)"
+esac
+
+readonly DATE=${news_date//[()]/} # bash feature
+manthdata ()
+{
+       set x $*
+       if [ $# != 7 ]
+       then
+               append_emsg "'$mp' has too many '.TH' lines"
+               man_mismatch=1
+       fi
+       man_date=${5-} man_version=${7-}
+}
+
+echo -n "Checking that manual page dates and versions are $DATE and $VERSION... "
+manfiles=`find man -type f | sort`
+man_pages_ok=Yes
+for mp in $manfiles
+do
+       case $mp in
+               *.[0-9]) ;; # fall below this 'case ... esac'
+
+               */Makefile.local | */Makefile ) continue ;;
+               */.gitignore)   continue ;;
+               *.bak)          continue ;;
+
+               *)      append_emsg "'$mp': extra file"
+                       man_pages_ok=No
+                       continue
+       esac
+       manthdata `sed -n '/^[.]TH NOTMUCH/ { y/"/ /; p; }' "$mp"`
+       if [ "$man_version" != "$VERSION" ]
+       then    append_emsg "Version '$man_version' is not '$VERSION' in $mp"
+               mman_pages_ok=No
+       fi
+       if [ "$man_date" != "$DATE" ]
+       then    append_emsg "DATE '$man_date' is not '$DATE' in $mp"
+               man_pages_ok=No
+       fi
+done
+echo $man_pages_ok.
+
+if [ -n "$emsgs" ]
+then
+       echo
+       echo 'Release check failed; check these issues:'
+       echo -e "$emsgs"
+       exit 1
+fi
+
+echo 'All checks this script executed completed successfully.'
+echo 'Make sure that everything else mentioned in RELEASING'
+echo 'file is in order, too.'
+
+
+# Local variables:
+# mode: shell-script
+# sh-basic-offset: 8
+# tab-width: 8
+# End:
+# vi: set sw=8 ts=8
index 977cea75396e540a0bc64afd099fddd6ed8ba410..2405756e43a724ca3f2d2fc576710afc34e8046f 100644 (file)
@@ -1,5 +1,5 @@
 This file describes the schemata used for notmuch's structured output
-format (currently JSON).
+format (currently JSON and S-Expressions).
 
 []'s indicate lists.  List items can be marked with a '?', meaning
 they are optional; or a '*', meaning there can be zero or more of that
@@ -8,6 +8,14 @@ values.  An object field marked '?' is optional.  |'s indicate
 alternates (e.g., int|string means something can be an int or a
 string).
 
+For S-Expression output, lists are printed delimited by () instead of
+[]. Objects are printed as p-lists, i.e. lists where the keys and values
+are interleaved. Keys are printed as keywords (symbols preceded by a
+colon), e.g. (:id "123" :time 54321 :from "foobar"). Null is printed as
+nil, true as t and false as nil.
+
+This is version 1 of the structured output format.
+
 Common non-terminals
 --------------------
 
@@ -32,13 +40,13 @@ thread = [thread_node*]
 
 # A message and its replies (show_messages)
 thread_node = [
-    message?,                 # present if --entire-thread or matched
+    message|null,             # null if not matched and not --entire-thread
     [thread_node*]            # children of message
 ]
 
-# A message (format_part_json)
+# A message (format_part_sprinter)
 message = {
-    # (format_message_json)
+    # (format_message_sprinter)
     id:             messageid,
     match:          bool,
     filename:      string,
@@ -47,10 +55,10 @@ message = {
     tags:           [string*],
 
     headers:        headers,
-    body:           [part]
+    body?:          [part]    # omitted if --body=false
 }
 
-# A MIME part (format_part_json)
+# A MIME part (format_part_sprinter)
 part = {
     id:             int|string, # part id (currently DFS part number)
 
@@ -69,23 +77,31 @@ part = {
     # A leaf part's body content is optional, but may be included if
     # it can be correctly encoded as a string.  Consumers should use
     # this in preference to fetching the part content separately.
-    content?:       string
+    content?:       string,
+    # If a leaf part's body content is not included, the length of
+    # the encoded content (in bytes) may be given instead.
+    content-length?: int,
+    # If a leaf part's body content is not included, its transfer encoding
+    # may be given.  Using this and the encoded content length, it is
+    # possible for the consumer to estimate the decoded content length.
+    content-transfer-encoding?: string
 }
 
-# The headers of a message or part (format_headers_json with reply = FALSE)
+# The headers of a message or part (format_headers_sprinter with reply = FALSE)
 headers = {
     Subject:        string,
     From:           string,
     To?:            string,
     Cc?:            string,
     Bcc?:           string,
+    Reply-To?:      string,
     Date:           string
 }
 
-# Encryption status (format_part_json)
+# Encryption status (format_part_sprinter)
 encstatus = [{status: "good"|"bad"}]
 
-# Signature status (format_part_sigstatus_json)
+# Signature status (format_part_sigstatus_sprinter)
 sigstatus = [signature*]
 
 signature = {
@@ -128,7 +144,8 @@ thread = {
     total:          int,      # total messages in thread
     authors:        string,   # comma-separated names with | between
                               # matched and unmatched
-    subject:        string
+    subject:        string,
+    tags:           [string*]
 }
 
 notmuch reply schema
@@ -138,11 +155,11 @@ reply = {
     # The headers of the constructed reply
     reply-headers: reply_headers,
 
-    # As in the show format (format_part_json)
+    # As in the show format (format_part_sprinter)
     original: message
 }
 
-# Reply headers (format_headers_json with reply = TRUE)
+# Reply headers (format_headers_sprinter with reply = TRUE)
 reply_headers = {
     Subject:        string,
     From:           string,
index d8075ba12743392ea6743225c686bf61abe2ef55..6a8769c63902e76835d27c5d32a0fd7a4e32595c 100644 (file)
@@ -1,13 +1,13 @@
 #
-# uncrustify config file for the linux kernel
+# Uncrustify config file for notmuch.
+# Based on uncrustify config file for the linux kernel
 #
 # $Id: linux-indent.cfg 488 2006-09-09 12:44:38Z bengardner $
 # Taken from the uncrustify distribution under license (GPL2+)
 #
-# sample usage:
+# Sample usage:
 #        uncrustify --replace -c uncrustify.cfg foo.c
 #
-#
 
 indent_with_tabs       = 2             # 1=indent to level only, 2=indent with tabs
 align_with_tabs                = TRUE          # use tabs to align
@@ -18,6 +18,8 @@ indent_columns                = 4
 
 indent_label           = -2            # pos: absolute col, neg: relative column
 
+indent_cmt_with_tabs   = false         # true would align to tabstop always...
+
 #
 # inter-symbol newlines
 #
@@ -54,11 +56,16 @@ nl_after_struct = 0
 # mod_full_brace_do    = remove        # "do a--; while ();" vs "do { a--; } while ();"
 # mod_full_brace_while = remove        # "while (a) a--;" vs "while (a) { a--; }"
 
-#
-# Extra types used in notmuch source.
-# (add more on demand)
 
-type GMimeObject mime_node_t
+# In case some custom types aren't detected properly by uncrustify
+# add those to this section below. For example there are cases where
+# uncrustify doesn't know whether a 'token' is part of pointer type
+# or left operand of a binary multiplication operation.
+
+type FILE
+type GMimeObject GMimeCryptoContext GMimeCipherContext
+type mime_node_t notmuch_message_t notmuch_show_params_t
+type sprinter_t
 
 #
 # inter-character spacing options
@@ -107,6 +114,6 @@ align_right_cmt_span        = 8             # align comments span this much in func
 # align_pp_define_span = 8;
 # align_pp_define_gap  = 4;
 
-# cmt_star_cont                = FALSE
+cmt_star_cont          = true
 
 # indent_brace         = 0
diff --git a/dump-restore-private.h b/dump-restore-private.h
new file mode 100644 (file)
index 0000000..896a004
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef DUMP_RESTORE_PRIVATE_H
+#define DUMP_RESTORE_PRIVATE_H
+
+#include "hex-escape.h"
+#include "command-line-arguments.h"
+
+typedef enum dump_formats {
+    DUMP_FORMAT_AUTO,
+    DUMP_FORMAT_BATCH_TAG,
+    DUMP_FORMAT_SUP
+} dump_format_t;
+
+#endif
index 684bedcb51960c6f2a779b7e66f90aa261fbc2ed..6db62a01c46b09e7851c9259552c13d7a7574c6b 100644 (file)
@@ -154,11 +154,6 @@ International Bureau of Weights and Measures."
 (defvar notmuch-hello-url "http://notmuchmail.org"
   "The `notmuch' web site.")
 
-(defvar notmuch-hello-search-pos nil
-  "Position of search widget, if any.
-
-This should only be set by `notmuch-hello-insert-search'.")
-
 (defvar notmuch-hello-custom-section-options
   '((:filter (string :tag "Filter for each tag"))
     (:filter-count (string :tag "Different filter to generate message counts"))
@@ -209,11 +204,8 @@ function produces a section simply by adding content to the current
 buffer.  A section should not end with an empty line, because a
 newline will be inserted after each section by `notmuch-hello'.
 
-Each function should take no arguments.  If the produced section
-includes `notmuch-hello-target' (i.e. cursor should be positioned
-inside this section), the function should return this element's
-position.
-Otherwise, it should return nil.
+Each function should take no arguments. The return value is
+ignored.
 
 For convenience an element can also be a list of the form (FUNC ARG1
 ARG2 .. ARGN) in which case FUNC will be applied to the rest of the
@@ -240,15 +232,6 @@ supported for \"Customized queries section\" items."
            notmuch-hello-query-section
            (function :tag "Custom section"))))
 
-(defvar notmuch-hello-target nil
-  "Button text at position of point before rebuilding the notmuch-buffer.
-
-This variable contains the text of the button, if any, the
-point was positioned at before the notmuch-hello buffer was
-rebuilt. This should never actually be global and is defined as a
-defvar only for documentation purposes and to avoid a compiler
-warning about it occurring as a free variable.")
-
 (defvar notmuch-hello-hidden-sections nil
   "List of sections titles whose contents are hidden")
 
@@ -435,8 +418,7 @@ Such a list can be computed with `notmuch-hello-query-counts'."
         (reordered-list (notmuch-hello-reflect searches tags-per-line))
         ;; Hack the display of the buttons used.
         (widget-push-button-prefix "")
-        (widget-push-button-suffix "")
-        (found-target-pos nil))
+        (widget-push-button-suffix ""))
     ;; dme: It feels as though there should be a better way to
     ;; implement this loop than using an incrementing counter.
     (mapc (lambda (elem)
@@ -449,8 +431,6 @@ Such a list can be computed with `notmuch-hello-query-counts'."
                     (msg-count (third elem)))
                (widget-insert (format "%8s "
                                       (notmuch-hello-nice-number msg-count)))
-               (if (string= name notmuch-hello-target)
-                   (setq found-target-pos (point-marker)))
                (widget-create 'push-button
                               :notify #'notmuch-hello-widget-search
                               :notmuch-search-terms query
@@ -466,8 +446,7 @@ Such a list can be computed with `notmuch-hello-query-counts'."
     ;; If the last line was not full (and hence did not include a
     ;; carriage return), insert one now.
     (unless (eq (% count tags-per-line) 0)
-      (widget-insert "\n"))
-    found-target-pos))
+      (widget-insert "\n"))))
 
 (defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png")))
 
@@ -521,7 +500,7 @@ Complete list of currently available key bindings:
 (defun notmuch-hello-generate-tag-alist (&optional hide-tags)
   "Return an alist from tags to queries to display in the all-tags section."
   (mapcar (lambda (tag)
-           (cons tag (format "tag:%s" tag)))
+           (cons tag (concat "tag:" (notmuch-escape-boolean-term tag))))
          (notmuch-remove-if-not
           (lambda (tag)
             (not (member tag hide-tags)))
@@ -571,8 +550,7 @@ Complete list of currently available key bindings:
                       (funcall notmuch-saved-search-sort-function
                                notmuch-saved-searches)
                     notmuch-saved-searches)
-                  :show-empty-searches notmuch-show-empty-saved-searches))
-       found-target-pos)
+                  :show-empty-searches notmuch-show-empty-saved-searches)))
     (when searches
       (widget-insert "Saved searches: ")
       (widget-create 'push-button
@@ -581,15 +559,12 @@ Complete list of currently available key bindings:
                     "edit")
       (widget-insert "\n\n")
       (let ((start (point)))
-       (setq found-target-pos
-             (notmuch-hello-insert-buttons searches))
-       (indent-rigidly start (point) notmuch-hello-indent)
-       found-target-pos))))
+       (notmuch-hello-insert-buttons searches)
+       (indent-rigidly start (point) notmuch-hello-indent)))))
 
 (defun notmuch-hello-insert-search ()
   "Insert a search widget."
   (widget-insert "Search: ")
-  (setq notmuch-hello-search-pos (point-marker))
   (widget-create 'editable-field
                 ;; Leave some space at the start and end of the
                 ;; search boxes.
@@ -689,16 +664,13 @@ Supports the following entries in OPTIONS as a plist:
                                (notmuch-hello-update))
                     "hide"))
     (widget-insert "\n")
-    (let (target-pos)
-      (when (not is-hidden)
-       (let ((searches (apply 'notmuch-hello-query-counts query-alist options)))
-         (when (or (not (plist-get options :hide-if-empty))
-                   searches)
-           (widget-insert "\n")
-           (setq target-pos
-                 (notmuch-hello-insert-buttons searches))
-           (indent-rigidly start (point) notmuch-hello-indent))))
-      target-pos)))
+    (when (not is-hidden)
+      (let ((searches (apply 'notmuch-hello-query-counts query-alist options)))
+       (when (or (not (plist-get options :hide-if-empty))
+                 searches)
+         (widget-insert "\n")
+         (notmuch-hello-insert-buttons searches)
+         (indent-rigidly start (point) notmuch-hello-indent))))))
 
 (defun notmuch-hello-insert-tags-section (&optional title &rest options)
   "Insert a section displaying all tags with message counts.
@@ -717,7 +689,7 @@ following:
   "Show an entry for each saved search and inboxed messages for each tag"
   (notmuch-hello-insert-searches "What's in your inbox"
                                 (append
-                                 (notmuch-saved-searches)
+                                 notmuch-saved-searches
                                  (notmuch-hello-generate-tag-alist))
                                 :filter "tag:inbox"))
 
@@ -754,22 +726,12 @@ following:
   "Run notmuch and display saved searches, known tags, etc."
   (interactive)
 
-  ;; Jump through a hoop to get this value from the deprecated variable
-  ;; name (`notmuch-folders') or from the default value.
-  (unless notmuch-saved-searches
-    (setq notmuch-saved-searches (notmuch-saved-searches)))
-
   (if no-display
       (set-buffer "*notmuch-hello*")
     (switch-to-buffer "*notmuch-hello*"))
 
-  (let ((notmuch-hello-target (if (widget-at)
-                                 (widget-value (widget-at))
-                               (condition-case nil
-                                   (progn
-                                     (widget-forward 1)
-                                     (widget-value (widget-at)))
-                                 (error nil))))
+  (let ((target-line (line-number-at-pos))
+       (target-column (current-column))
        (inhibit-read-only t))
 
     ;; Delete all editable widget fields.  Editable widget fields are
@@ -788,30 +750,25 @@ following:
       (mapc 'delete-overlay (car all))
       (mapc 'delete-overlay (cdr all)))
 
-    (let (final-target-pos)
-      (mapc
-       (lambda (section)
-        (let ((point-before (point))
-              (result (if (functionp section)
-                          (funcall section)
-                        (apply (car section) (cdr section)))))
-          (if (and (not final-target-pos) (integer-or-marker-p result))
-              (setq final-target-pos result))
-          ;; don't insert a newline when the previous section didn't show
-          ;; anything.
-          (unless (eq (point) point-before)
-            (widget-insert "\n"))))
-       notmuch-hello-sections)
-      (widget-setup)
-
-      (when final-target-pos
-       (goto-char final-target-pos)
-       (unless (widget-at)
-         (widget-forward 1)))
-
-      (unless (widget-at)
-       (when notmuch-hello-search-pos
-         (goto-char notmuch-hello-search-pos)))))
+    (mapc
+     (lambda (section)
+       (let ((point-before (point)))
+        (if (functionp section)
+            (funcall section)
+          (apply (car section) (cdr section)))
+        ;; don't insert a newline when the previous section didn't
+        ;; show anything.
+        (unless (eq (point) point-before)
+          (widget-insert "\n"))))
+     notmuch-hello-sections)
+    (widget-setup)
+
+    ;; Move point back to where it was before refresh. Use line and
+    ;; column instead of point directly to be insensitive to additions
+    ;; and removals of text within earlier lines.
+    (goto-char (point-min))
+    (forward-line (1- target-line))
+    (move-to-column target-column))
   (run-hooks 'notmuch-hello-refresh-hook)
   (setq notmuch-hello-first-run nil))
 
index e99b48d107e164b1ab56c92dc9da654f75d862e4..d78bcf8065aae699a162241e9e7e92b114a3267a 100644 (file)
@@ -23,7 +23,8 @@
 
 (require 'mm-view)
 (require 'mm-decode)
-(eval-when-compile (require 'cl))
+(require 'json)
+(require 'cl)
 
 (defvar notmuch-command "notmuch"
   "Command to run the notmuch binary.")
 (defvar notmuch-search-history nil
   "Variable to store notmuch searches history.")
 
-(defcustom notmuch-saved-searches nil
+(defcustom notmuch-saved-searches '(("inbox" . "tag:inbox")
+                                   ("unread" . "tag:unread"))
   "A list of saved searches to display."
   :type '(alist :key-type string :value-type string)
   :group 'notmuch-hello)
 
-(defvar notmuch-folders nil
-  "Deprecated name for what is now known as `notmuch-saved-searches'.")
+(defcustom notmuch-archive-tags '("-inbox")
+  "List of tag changes to apply to a message or a thread when it is archived.
 
-(defun notmuch-saved-searches ()
-  "Common function for querying the notmuch-saved-searches variable.
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message or thread being archived.
 
-We do this as a function to support the old name of the
-variable (`notmuch-folders') as well as for the default value if
-the user hasn't set this variable with the old or new value."
-  (if notmuch-saved-searches
-      notmuch-saved-searches
-    (if notmuch-folders
-       notmuch-folders
-      '(("inbox" . "tag:inbox")
-       ("unread" . "tag:unread")))))
+For example, if you wanted to remove an \"inbox\" tag and add an
+\"archived\" tag, you would set:
+    (\"-inbox\" \"+archived\")"
+  :type '(repeat string)
+  :group 'notmuch-search
+  :group 'notmuch-show)
 
 (defun notmuch-version ()
   "Return a string with the notmuch version number."
@@ -146,16 +146,36 @@ the user hasn't set this variable with the old or new value."
        "[No Subject]"
       subject)))
 
+(defun notmuch-escape-boolean-term (term)
+  "Escape a boolean term for use in a query.
+
+The caller is responsible for prepending the term prefix and a
+colon.  This performs minimal escaping in order to produce
+user-friendly queries."
+
+  (save-match-data
+    (if (or (equal term "")
+           (string-match "[ ()]\\|^\"" term))
+       ;; Requires escaping
+       (concat "\"" (replace-regexp-in-string "\"" "\"\"" term t t) "\"")
+      term)))
+
 (defun notmuch-id-to-query (id)
   "Return a query that matches the message with id ID."
-  (concat "id:\"" (replace-regexp-in-string "\"" "\"\"" id t t) "\""))
+  (concat "id:" (notmuch-escape-boolean-term id)))
 
 ;;
 
 (defun notmuch-common-do-stash (text)
   "Common function to stash text in kill ring, and display in minibuffer."
-  (kill-new text)
-  (message "Stashed: %s" text))
+  (if text
+      (progn
+       (kill-new text)
+       (message "Stashed: %s" text))
+    ;; There is nothing to stash so stash an empty string so the user
+    ;; doesn't accidentally paste something else somewhere.
+    (kill-new "")
+    (message "Nothing to stash!")))
 
 ;;
 
@@ -239,6 +259,19 @@ the given type."
   (or (plist-get part :content)
       (notmuch-get-bodypart-internal (notmuch-id-to-query (plist-get msg :id)) nth process-crypto)))
 
+;; Workaround: The call to `mm-display-part' below triggers a bug in
+;; Emacs 24 if it attempts to use the shr renderer to display an HTML
+;; part with images in it (demonstrated in 24.1 and 24.2 on Debian and
+;; Fedora 17, though unreproducable in other configurations).
+;; `mm-shr' references the variable `gnus-inhibit-images' without
+;; first loading gnus-art, which defines it, resulting in a
+;; void-variable error.  Hence, we advise `mm-shr' to ensure gnus-art
+;; is loaded.
+(if (>= emacs-major-version 24)
+    (defadvice mm-shr (before load-gnus-arts activate)
+      (require 'gnus-art nil t)
+      (ad-disable-advice 'mm-shr 'before 'load-gnus-arts)))
+
 (defun notmuch-mm-display-part-inline (msg part nth content-type process-crypto)
   "Use the mm-decode/mm-view functions to display a part in the
 current buffer, if possible."
@@ -268,6 +301,130 @@ current buffer, if possible."
   (loop for (key value . rest) on plist by #'cddr
        collect (cons (intern (substring (symbol-name key) 1)) value)))
 
+(defun notmuch-combine-face-text-property (start end face)
+  "Combine FACE into the 'face text property between START and END.
+
+This function combines FACE with any existing faces between START
+and END.  Attributes specified by FACE take precedence over
+existing attributes.  FACE must be a face name (a symbol or
+string), a property list of face attributes, or a list of these."
+
+  (let ((pos start))
+    (while (< pos end)
+      (let ((cur (get-text-property pos 'face))
+           (next (next-single-property-change pos 'face nil end)))
+       (put-text-property pos next 'face (cons face cur))
+       (setq pos next)))))
+
+(defun notmuch-logged-error (msg &optional extra)
+  "Log MSG and EXTRA to *Notmuch errors* and signal MSG.
+
+This logs MSG and EXTRA to the *Notmuch errors* buffer and
+signals MSG as an error.  If EXTRA is non-nil, text referring the
+user to the *Notmuch errors* buffer will be appended to the
+signaled error.  This function does not return."
+
+  (with-current-buffer (get-buffer-create "*Notmuch errors*")
+    (goto-char (point-max))
+    (unless (bobp)
+      (newline))
+    (save-excursion
+      (insert "[" (current-time-string) "]\n" msg)
+      (unless (bolp)
+       (newline))
+      (when extra
+       (insert extra)
+       (unless (bolp)
+         (newline)))))
+  (error "%s" (concat msg (when extra
+                           " (see *Notmuch errors* for more details)"))))
+
+(defun notmuch-check-async-exit-status (proc msg)
+  "If PROC exited abnormally, pop up an error buffer and signal an error.
+
+This is a wrapper around `notmuch-check-exit-status' for
+asynchronous process sentinels.  PROC and MSG must be the
+arguments passed to the sentinel."
+  (let ((exit-status
+        (case (process-status proc)
+          ((exit) (process-exit-status proc))
+          ((signal) msg))))
+    (when exit-status
+      (notmuch-check-exit-status exit-status (process-command proc)))))
+
+(defun notmuch-check-exit-status (exit-status command &optional output err-file)
+  "If EXIT-STATUS is non-zero, pop up an error buffer and signal an error.
+
+If EXIT-STATUS is non-zero, pop up a notmuch error buffer
+describing the error and signal an Elisp error.  EXIT-STATUS must
+be a number indicating the exit status code of a process or a
+string describing the signal that terminated the process (such as
+returned by `call-process').  COMMAND must be a list giving the
+command and its arguments.  OUTPUT, if provided, is a string
+giving the output of command.  ERR-FILE, if provided, is the name
+of a file containing the error output of command.  OUTPUT and the
+contents of ERR-FILE will be included in the error message."
+
+  (cond
+   ((eq exit-status 0) t)
+   ((eq exit-status 20)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested an older output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch Emacs package."))
+   ((eq exit-status 21)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested a newer output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch package."))
+   (t
+    (let* ((err (when err-file
+                 (with-temp-buffer
+                   (insert-file-contents err-file)
+                   (unless (eobp)
+                     (buffer-string)))))
+          (extra
+           (concat
+            "command: " (mapconcat #'shell-quote-argument command " ") "\n"
+            (if (integerp exit-status)
+                (format "exit status: %s\n" exit-status)
+              (format "exit signal: %s\n" exit-status))
+            (when err
+              (concat "stderr:\n" err))
+            (when output
+              (concat "stdout:\n" output)))))
+       (if err
+           ;; We have an error message straight from the CLI.
+           (notmuch-logged-error
+            (replace-regexp-in-string "\\s $" "" err) extra)
+         ;; We only have combined output from the CLI; don't inundate
+         ;; the user with it.  Mimic `process-lines'.
+         (notmuch-logged-error (format "%s exited with status %s"
+                                       (car command) exit-status)
+                               extra))
+       ;; `notmuch-logged-error' does not return.
+       ))))
+
+(defun notmuch-call-notmuch-json (&rest args)
+  "Invoke `notmuch-command' with `args' and return the parsed JSON output.
+
+The returned output will represent objects using property lists
+and arrays as lists.  If notmuch exits with a non-zero status,
+this will pop up a buffer containing notmuch's output and signal
+an error."
+
+  (with-temp-buffer
+    (let ((err-file (make-temp-file "nmerr")))
+      (unwind-protect
+         (let ((status (apply #'call-process
+                              notmuch-command nil (list t err-file) nil args)))
+           (notmuch-check-exit-status status (cons notmuch-command args)
+                                      (buffer-string) err-file)
+           (goto-char (point-min))
+           (let ((json-object-type 'plist)
+                 (json-array-type 'list)
+                 (json-false 'nil))
+             (json-read)))
+       (delete-file err-file)))))
+
 ;; Compatibility functions for versions of emacs before emacs 23.
 ;;
 ;; Both functions here were copied from emacs 23 with the following copyright:
@@ -296,5 +453,274 @@ was called."
 (defvar notmuch-show-process-crypto nil)
 (make-variable-buffer-local 'notmuch-show-process-crypto)
 
+;; Incremental JSON parsing
+
+;; These two variables are internal variables to the parsing
+;; routines. They are always used buffer local but need to be declared
+;; globally to avoid compiler warnings.
+
+(defvar notmuch-json-parser nil
+  "Internal incremental JSON parser object: local to the buffer being parsed.")
+
+(defvar notmuch-json-state nil
+  "State of the internal JSON parser: local to the buffer being parsed.")
+
+(defun notmuch-json-create-parser (buffer)
+  "Return a streaming JSON parser that consumes input from BUFFER.
+
+This parser is designed to read streaming JSON whose structure is
+known to the caller.  Like a typical JSON parsing interface, it
+provides a function to read a complete JSON value from the input.
+However, it extends this with an additional function that
+requires the next value in the input to be a compound value and
+descends into it, allowing its elements to be read one at a time
+or further descended into.  Both functions can return 'retry to
+indicate that not enough input is available.
+
+The parser always consumes input from BUFFER's point.  Hence, the
+caller is allowed to delete and data before point and may
+resynchronize after an error by moving point."
+
+  (list buffer
+       ;; Terminator stack: a stack of characters that indicate the
+       ;; end of the compound values enclosing point
+       '()
+       ;; Next: One of
+       ;; * 'expect-value if the next token must be a value, but a
+       ;;   value has not yet been reached
+       ;; * 'value if point is at the beginning of a value
+       ;; * 'expect-comma if the next token must be a comma
+       'expect-value
+       ;; Allow terminator: non-nil if the next token may be a
+       ;; terminator
+       nil
+       ;; Partial parse position: If state is 'value, a marker for
+       ;; the position of the partial parser or nil if no partial
+       ;; parsing has happened yet
+       nil
+       ;; Partial parse state: If state is 'value, the current
+       ;; `parse-partial-sexp' state
+       nil))
+
+(defmacro notmuch-json-buffer (jp) `(first ,jp))
+(defmacro notmuch-json-term-stack (jp) `(second ,jp))
+(defmacro notmuch-json-next (jp) `(third ,jp))
+(defmacro notmuch-json-allow-term (jp) `(fourth ,jp))
+(defmacro notmuch-json-partial-pos (jp) `(fifth ,jp))
+(defmacro notmuch-json-partial-state (jp) `(sixth ,jp))
+
+(defvar notmuch-json-syntax-table
+  (let ((table (make-syntax-table)))
+    ;; The standard syntax table is what we need except that "." needs
+    ;; to have word syntax instead of punctuation syntax.
+    (modify-syntax-entry ?. "w" table)
+    table)
+  "Syntax table used for incremental JSON parsing.")
+
+(defun notmuch-json-scan-to-value (jp)
+  ;; Helper function that consumes separators, terminators, and
+  ;; whitespace from point.  Returns nil if it successfully reached
+  ;; the beginning of a value, 'end if it consumed a terminator, or
+  ;; 'retry if not enough input was available to reach a value.  Upon
+  ;; nil return, (notmuch-json-next jp) is always 'value.
+
+  (if (eq (notmuch-json-next jp) 'value)
+      ;; We're already at a value
+      nil
+    ;; Drive the state toward 'expect-value
+    (skip-chars-forward " \t\r\n")
+    (or (when (eobp) 'retry)
+       ;; Test for the terminator for the current compound
+       (when (and (notmuch-json-allow-term jp)
+                  (eq (char-after) (car (notmuch-json-term-stack jp))))
+         ;; Consume it and expect a comma or terminator next
+         (forward-char)
+         (setf (notmuch-json-term-stack jp) (cdr (notmuch-json-term-stack jp))
+               (notmuch-json-next jp) 'expect-comma
+               (notmuch-json-allow-term jp) t)
+         'end)
+       ;; Test for a separator
+       (when (eq (notmuch-json-next jp) 'expect-comma)
+         (when (/= (char-after) ?,)
+           (signal 'json-readtable-error (list "expected ','")))
+         ;; Consume it, switch to 'expect-value, and disallow a
+         ;; terminator
+         (forward-char)
+         (skip-chars-forward " \t\r\n")
+         (setf (notmuch-json-next jp) 'expect-value
+               (notmuch-json-allow-term jp) nil)
+         ;; We moved point, so test for eobp again and fall through
+         ;; to the next test if there's more input
+         (when (eobp) 'retry))
+       ;; Next must be 'expect-value and we know this isn't
+       ;; whitespace, EOB, or a terminator, so point must be on a
+       ;; value
+       (progn
+         (assert (eq (notmuch-json-next jp) 'expect-value))
+         (setf (notmuch-json-next jp) 'value)
+         nil))))
+
+(defun notmuch-json-begin-compound (jp)
+  "Parse the beginning of a compound value and traverse inside it.
+
+Returns 'retry if there is insufficient input to parse the
+beginning of the compound.  If this is able to parse the
+beginning of a compound, it moves point past the token that opens
+the compound and returns t.  Later calls to `notmuch-json-read'
+will return the compound's elements.
+
+Entering JSON objects is currently unimplemented."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    ;; Disallow terminators
+    (setf (notmuch-json-allow-term jp) nil)
+    ;; Save "next" so we can restore it if there's a syntax error
+    (let ((saved-next (notmuch-json-next jp)))
+      (or (notmuch-json-scan-to-value jp)
+         (if (/= (char-after) ?\[)
+             (progn
+               (setf (notmuch-json-next jp) saved-next)
+               (signal 'json-readtable-error (list "expected '['")))
+           (forward-char)
+           (push ?\] (notmuch-json-term-stack jp))
+           ;; Expect a value or terminator next
+           (setf (notmuch-json-next jp) 'expect-value
+                 (notmuch-json-allow-term jp) t)
+           t)))))
+
+(defun notmuch-json-read (jp)
+  "Parse the value at point in JP's buffer.
+
+Returns 'retry if there is insufficient input to parse a complete
+JSON value (though it may still move point over separators or
+whitespace).  If the parser is currently inside a compound value
+and the next token ends the list or object, this moves point just
+past the terminator and returns 'end.  Otherwise, this moves
+point to just past the end of the value and returns the value."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    (or
+     ;; Get to a value state
+     (notmuch-json-scan-to-value jp)
+
+     ;; Can we parse a complete value?
+     (let ((complete
+           (if (looking-at "[-+0-9tfn]")
+               ;; This is a number or a keyword, so the partial
+               ;; parser isn't going to help us because a truncated
+               ;; number or keyword looks like a complete symbol to
+               ;; it.  Look for something that clearly ends it.
+               (save-excursion
+                 (skip-chars-forward "^]},: \t\r\n")
+                 (not (eobp)))
+
+             ;; We're looking at a string, object, or array, which we
+             ;; can partial parse.  If we just reached the value, set
+             ;; up the partial parser.
+             (when (null (notmuch-json-partial-state jp))
+               (setf (notmuch-json-partial-pos jp) (point-marker)))
+
+             ;; Extend the partial parse until we either reach EOB or
+             ;; get the whole value
+             (save-excursion
+               (let ((pstate
+                      (with-syntax-table notmuch-json-syntax-table
+                        (parse-partial-sexp
+                         (notmuch-json-partial-pos jp) (point-max) 0 nil
+                         (notmuch-json-partial-state jp)))))
+                 ;; A complete value is available if we've reached
+                 ;; depth 0 or less and encountered a complete
+                 ;; subexpression.
+                 (if (and (<= (first pstate) 0) (third pstate))
+                     t
+                   ;; Not complete.  Update the partial parser state
+                   (setf (notmuch-json-partial-pos jp) (point-marker)
+                         (notmuch-json-partial-state jp) pstate)
+                   nil))))))
+
+       (if (not complete)
+          'retry
+        ;; We have a value.  Reset the partial parse state and expect
+        ;; a comma or terminator after the value.
+        (setf (notmuch-json-next jp) 'expect-comma
+              (notmuch-json-allow-term jp) t
+              (notmuch-json-partial-pos jp) nil
+              (notmuch-json-partial-state jp) nil)
+        ;; Parse the value
+        (let ((json-object-type 'plist)
+              (json-array-type 'list)
+              (json-false nil))
+          (json-read)))))))
+
+(defun notmuch-json-eof (jp)
+  "Signal a json-error if there is more data in JP's buffer.
+
+Moves point to the beginning of any trailing data or to the end
+of the buffer if there is only trailing whitespace."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    (skip-chars-forward " \t\r\n")
+    (unless (eobp)
+      (signal 'json-error (list "Trailing garbage following JSON data")))))
+
+(defun notmuch-json-parse-partial-list (result-function error-function results-buf)
+  "Parse a partial JSON list from current buffer.
+
+This function consumes a JSON list from the current buffer,
+applying RESULT-FUNCTION in buffer RESULT-BUFFER to each complete
+value in the list.  It operates incrementally and should be
+called whenever the buffer has been extended with additional
+data.
+
+If there is a syntax error, this will attempt to resynchronize
+with the input and will apply ERROR-FUNCTION in buffer
+RESULT-BUFFER to any input that was skipped.
+
+It sets up all the needed internal variables: the caller just
+needs to call it with point in the same place that the parser
+left it."
+  (let (done)
+    (unless (local-variable-p 'notmuch-json-parser)
+      (set (make-local-variable 'notmuch-json-parser)
+          (notmuch-json-create-parser (current-buffer)))
+      (set (make-local-variable 'notmuch-json-state) 'begin))
+    (while (not done)
+      (condition-case nil
+         (case notmuch-json-state
+               ((begin)
+                ;; Enter the results list
+                (if (eq (notmuch-json-begin-compound
+                         notmuch-json-parser) 'retry)
+                    (setq done t)
+                  (setq notmuch-json-state 'result)))
+               ((result)
+                ;; Parse a result
+                (let ((result (notmuch-json-read notmuch-json-parser)))
+                  (case result
+                        ((retry) (setq done t))
+                        ((end) (setq notmuch-json-state 'end))
+                        (otherwise (with-current-buffer results-buf
+                                     (funcall result-function result))))))
+               ((end)
+                ;; Any trailing data is unexpected
+                (notmuch-json-eof notmuch-json-parser)
+                (setq done t)))
+       (json-error
+        ;; Do our best to resynchronize and ensure forward
+        ;; progress
+        (let ((bad (buffer-substring (line-beginning-position)
+                                     (line-end-position))))
+          (forward-line)
+          (with-current-buffer results-buf
+            (funcall error-function "%s" bad))))))
+    ;; Clear out what we've parsed
+    (delete-region (point-min) (point))))
+
+
+
+
 (provide 'notmuch-lib)
 
+;; Local Variables:
+;; byte-compile-warnings: (not cl-functions)
+;; End:
index dcfbc4b373fddbddb1d88588913f73f063c5cc22..07eedba22a33a28ae802f260781c3fbfdeeca582 100644 (file)
@@ -140,13 +140,12 @@ will NOT be removed or replaced."
                            t))
 
 (defun notmuch-maildir-fcc-make-uniq-maildir-id ()
-   (let* ((ct (current-time))
-         (timeid (+ (* (car ct) 65536) (cadr ct)))
-         (microseconds (car (cdr (cdr ct))))
+   (let* ((ftime (float-time))
+         (microseconds (mod (* 1000000 ftime) 1000000))
          (hostname (notmuch-maildir-fcc-host-fixer system-name)))
      (setq notmuch-maildir-fcc-count (+ notmuch-maildir-fcc-count 1))
      (format "%d.%d_%d_%d.%s"
-            timeid
+            ftime
             (emacs-pid)
             microseconds
             notmuch-maildir-fcc-count
index 5964caa3ce5b945d8c4aa2182889ef0f100ce069..4dc48832f1412ec4a1dfd41fcee64b7f70a06549 100644 (file)
 (require 'notmuch-tag)
 (require 'notmuch-mua)
 
-(defcustom notmuch-message-replied-tags '("replied")
-  "Tags to be automatically added to or removed from a message when it is replied to.
-Any tag in the list will be added to a replied message or,
-if it is prefaced with a \"-\", removed.
+(defcustom notmuch-message-replied-tags '("+replied")
+  "List of tag changes to apply to a message when it has been replied to.
+
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message being replied to.
 
 For example, if you wanted to add a \"replied\" tag and remove
-the \"inbox\" and \"todo\", you would set
-    (\"replied\" \"-inbox\" \"-todo\"\)"
-  :type 'list
+the \"inbox\" and \"todo\" tags, you would set:
+    (\"+replied\" \"-inbox\" \"-todo\"\)"
+  :type '(repeat string)
   :group 'notmuch-send)
 
 (defun notmuch-message-mark-replied ()
   ;; get the in-reply-to header and parse it for the message id.
   (let ((rep (mail-header-parse-addresses (message-field-value "In-Reply-To"))))
     (when (and notmuch-message-replied-tags rep)
-      ;; add a "+" to any tag that is doesn't already begin with a "+"
-      ;; or "-"
-      (let ((tags (mapcar (lambda (str)
-                           (if (not (string-match "^[+-]" str))
-                               (concat "+" str)
-                             str))
-                         notmuch-message-replied-tags)))
-       (apply 'notmuch-tag (notmuch-id-to-query (car (car rep))) tags)))))
+      (funcall 'notmuch-tag (notmuch-id-to-query (car (car rep)))
+              (notmuch-tag-change-list notmuch-message-replied-tags)))))
 
 (add-hook 'message-send-hook 'notmuch-message-mark-replied)
 
index 408b49e075068c574f900a756d76551c87ee7eb5..24eebffa3b97699d0d566d85f8c17dc40fc69642 100644 (file)
@@ -146,7 +146,7 @@ list."
   (unless (bolp) (insert "\n")))
 
 (defun notmuch-mua-reply (query-string &optional sender reply-all)
-  (let ((args '("reply" "--format=json"))
+  (let ((args '("reply" "--format=json" "--format-version=1"))
        reply
        original)
     (when notmuch-show-process-crypto
@@ -158,13 +158,7 @@ list."
     (setq args (append args (list query-string)))
 
     ;; Get the reply object as JSON, and parse it into an elisp object.
-    (with-temp-buffer
-      (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
-      (goto-char (point-min))
-      (let ((json-object-type 'plist)
-           (json-array-type 'list)
-           (json-false 'nil))
-       (setq reply (json-read))))
+    (setq reply (apply #'notmuch-call-notmuch-json args))
 
     ;; Extract the original message to simplify the following code.
     (setq original (plist-get reply :original))
index d66baeab983b06a6be96abd7ae4bd75c945718a9..6e9f406d5c8fddf8559d8fc666af2929b4163ede 100644 (file)
@@ -29,18 +29,11 @@ A thread is a forest or list of trees. A tree is a two element
 list where the first element is a message, and the second element
 is a possibly empty forest of replies.
 "
-  (let  ((args '("show" "--format=json"))
-        (json-object-type 'plist)
-        (json-array-type 'list)
-        (json-false 'nil))
+  (let ((args '("show" "--format=json" "--format-version=1")))
     (if notmuch-show-process-crypto
        (setq args (append args '("--decrypt"))))
     (setq args (append args search-terms))
-    (with-temp-buffer
-      (progn
-       (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
-       (goto-char (point-min))
-       (json-read)))))
+    (apply #'notmuch-call-notmuch-json args)))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; Mapping functions across collections of messages.
index d318430cdc842c71acf8acab6262c46d066d732b..1864dd15a05bb8b24c2255806c9e383d9f5f6e31 100644 (file)
@@ -47,8 +47,8 @@
 
 For an open message, all of these headers will be made visible
 according to `notmuch-message-headers-visible' or can be toggled
-with `notmuch-show-toggle-headers'. For a closed message, only
-the first header in the list will be visible."
+with `notmuch-show-toggle-visibility-headers'. For a closed message,
+only the first header in the list will be visible."
   :type '(repeat string)
   :group 'notmuch-show)
 
@@ -58,8 +58,8 @@ the first header in the list will be visible."
 If this value is non-nil, then all of the headers defined in
 `notmuch-message-headers' will be visible by default in the display
 of each message. Otherwise, these headers will be hidden and
-`notmuch-show-toggle-headers' can be used to make the visible for
-any given message."
+`notmuch-show-toggle-visibility-headers' can be used to make them
+visible for any given message."
   :type 'boolean
   :group 'notmuch-show)
 
@@ -94,7 +94,7 @@ any given message."
   :group 'notmuch-hooks)
 
 ;; Mostly useful for debugging.
-(defcustom notmuch-show-all-multipart/alternative-parts t
+(defcustom notmuch-show-all-multipart/alternative-parts nil
   "Should all parts of multipart/alternative parts be shown?"
   :type 'boolean
   :group 'notmuch-show)
@@ -183,15 +183,30 @@ provided with an MLA argument nor `completing-read' input."
             notmuch-show-stash-mlarchive-link-alist))
   :group 'notmuch-show)
 
+(defcustom notmuch-show-mark-read-tags '("-unread")
+  "List of tag changes to apply to a message when it is marked as read.
+
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message being marked as read.
+
+For example, if you wanted to remove an \"unread\" tag and add a
+\"read\" tag (which would make little sense), you would set:
+    (\"-unread\" \"+read\")"
+  :type '(repeat string)
+  :group 'notmuch-show)
+
+
 (defmacro with-current-notmuch-show-message (&rest body)
   "Evaluate body with current buffer set to the text of current message"
   `(save-excursion
      (let ((id (notmuch-show-get-message-id)))
        (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
          (with-current-buffer buf
-           (call-process notmuch-command nil t nil "show" "--format=raw" id)
-           ,@body)
-        (kill-buffer buf)))))
+          (let ((coding-system-for-read 'no-conversion))
+            (call-process notmuch-command nil t nil "show" "--format=raw" id)
+            ,@body)
+          (kill-buffer buf))))))
 
 (defun notmuch-show-turn-on-visual-line-mode ()
   "Enable Visual Line mode."
@@ -351,9 +366,10 @@ operation on the contents of the current buffer."
                                             'face 'notmuch-tag-face)
                                 ")"))))))
 
-(defun notmuch-show-clean-address (address)
-  "Try to clean a single email ADDRESS for display.  Return
-unchanged ADDRESS if parsing fails."
+(defun notmuch-clean-address (address)
+  "Try to clean a single email ADDRESS for display. Return a cons
+cell of (AUTHOR_EMAIL AUTHOR_NAME). Return (ADDRESS nil) if
+parsing fails."
   (condition-case nil
     (let (p-name p-address)
       ;; It would be convenient to use `mail-header-parse-address',
@@ -401,12 +417,20 @@ unchanged ADDRESS if parsing fails."
       (when (string= p-name p-address)
        (setq p-name nil))
 
-      ;; If no name results, return just the address.
-      (if (not p-name)
-         p-address
-       ;; Otherwise format the name and address together.
-       (concat p-name " <" p-address ">")))
-    (error address)))
+      (cons p-address p-name))
+    (error (cons address nil))))
+
+(defun notmuch-show-clean-address (address)
+  "Try to clean a single email ADDRESS for display.  Return
+unchanged ADDRESS if parsing fails."
+  (let* ((clean-address (notmuch-clean-address address))
+        (p-address (car clean-address))
+        (p-name (cdr clean-address)))
+    ;; If no name, return just the address.
+    (if (not p-name)
+       p-address
+      ;; Otherwise format the name and address together.
+      (concat p-name " <" p-address ">"))))
 
 (defun notmuch-show-insert-headerline (headers date tags depth)
   "Insert a notmuch style headerline based on HEADERS for a
@@ -453,22 +477,23 @@ message at DEPTH in the current thread."
     (define-key map "s" 'notmuch-show-part-button-save)
     (define-key map "v" 'notmuch-show-part-button-view)
     (define-key map "o" 'notmuch-show-part-button-interactively-view)
+    (define-key map "|" 'notmuch-show-part-button-pipe)
     map)
   "Submap for button commands")
 (fset 'notmuch-show-part-button-map notmuch-show-part-button-map)
 
 (defun notmuch-show-insert-part-header (nth content-type declared-type &optional name comment)
-  (let ((button))
+  (let ((button)
+       (base-label (concat (when name (concat name ": "))
+                           declared-type
+                           (unless (string-equal declared-type content-type)
+                             (concat " (as " content-type ")"))
+                           comment)))
+
     (setq button
          (insert-button
-          (concat "[ "
-                  (if name (concat name ": ") "")
-                  declared-type
-                  (if (not (string-equal declared-type content-type))
-                      (concat " (as " content-type ")")
-                    "")
-                  (or comment "")
-                  " ]")
+          (concat "[ " base-label " ]")
+          :base-label base-label
           :type 'notmuch-show-part-button-type
           :notmuch-part nth
           :notmuch-filename name
@@ -524,6 +549,30 @@ message at DEPTH in the current thread."
     (let ((handle (mm-make-handle (current-buffer) (list content-type))))
       (mm-interactively-view-part handle))))
 
+(defun notmuch-show-pipe-part (message-id nth &optional filename content-type)
+  (notmuch-with-temp-part-buffer message-id nth
+    (let ((handle (mm-make-handle (current-buffer) (list content-type))))
+      (mm-pipe-part handle))))
+
+;; This is taken from notmuch-wash: maybe it should be unified?
+(defun notmuch-show-toggle-part-invisibility (&optional button)
+  (interactive)
+  (let* ((button (or button (button-at (point))))
+        (overlay (button-get button 'overlay)))
+    (when overlay
+      (let* ((show (overlay-get overlay 'invisible))
+            (new-start (button-start button))
+            (button-label (button-get button :base-label))
+            (old-point (point))
+            (inhibit-read-only t))
+       (overlay-put overlay 'invisible (not show))
+       (goto-char new-start)
+       (insert "[ " button-label (if show " ]" " (hidden) ]"))
+       (let ((old-end (button-end button)))
+         (move-overlay button new-start (point))
+         (delete-region (point) old-end))
+       (goto-char (min old-point (1- (button-end button))))))))
+
 (defun notmuch-show-multipart/*-to-list (part)
   (mapcar (lambda (inner-part) (plist-get inner-part :content-type))
          (plist-get part :content)))
@@ -537,11 +586,10 @@ message at DEPTH in the current thread."
     ;; but it's not clear that this is the wrong thing to do - which
     ;; should be chosen if there are more than one that match?
     (mapc (lambda (inner-part)
-           (let ((inner-type (plist-get inner-part :content-type)))
-             (if (or notmuch-show-all-multipart/alternative-parts
-                     (string= chosen-type inner-type))
-                 (notmuch-show-insert-bodypart msg inner-part depth)
-               (notmuch-show-insert-part-header (plist-get inner-part :id) inner-type inner-type nil " (not shown)"))))
+           (let* ((inner-type (plist-get inner-part :content-type))
+                 (hide (not (or notmuch-show-all-multipart/alternative-parts
+                          (string= chosen-type inner-type)))))
+             (notmuch-show-insert-bodypart msg inner-part depth hide)))
          inner-parts)
 
     (when notmuch-show-indent-multipart
@@ -727,17 +775,22 @@ message at DEPTH in the current thread."
   (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename))
   (insert (with-temp-buffer
            (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
+           ;; notmuch-get-bodypart-content provides "raw", non-converted
+           ;; data. Replace CRLF with LF before icalendar can use it.
            (goto-char (point-min))
+           (while (re-search-forward "\r\n" nil t)
+             (replace-match "\n" nil nil))
            (let ((file (make-temp-file "notmuch-ical"))
                  result)
-             (icalendar--convert-ical-to-diary
-              (icalendar--read-element nil nil)
-              file t)
-             (set-buffer (get-file-buffer file))
-             (setq result (buffer-substring (point-min) (point-max)))
-             (set-buffer-modified-p nil)
-             (kill-buffer (current-buffer))
-             (delete-file file)
+             (unwind-protect
+                 (progn
+                   (unless (icalendar-import-buffer file t)
+                     (error "Icalendar import error. See *icalendar-errors* for more information"))
+                   (set-buffer (get-file-buffer file))
+                   (setq result (buffer-substring (point-min) (point-max)))
+                   (set-buffer-modified-p nil)
+                   (kill-buffer (current-buffer)))
+               (delete-file file))
              result)))
   t)
 
@@ -765,6 +818,16 @@ message at DEPTH in the current thread."
 (defun notmuch-show-insert-part-inline-patch-fake-part (msg part content-type nth depth declared-type)
   (notmuch-show-insert-part-*/* msg part "text/x-diff" nth depth "inline patch"))
 
+(defun notmuch-show-insert-part-text/html (msg part content-type nth depth declared-type)
+  ;; text/html handler to work around bugs in renderers and our
+  ;; invisibile parts code. In particular w3m sets up a keymap which
+  ;; "leaks" outside the invisible region and causes strange effects
+  ;; in notmuch. We set mm-inline-text-html-with-w3m-keymap to nil to
+  ;; tell w3m not to set a keymap (so the normal notmuch-show-mode-map
+  ;; remains).
+  (let ((mm-inline-text-html-with-w3m-keymap nil))
+    (notmuch-show-insert-part-*/* msg part content-type nth depth declared-type)))
+
 (defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
   ;; This handler _must_ succeed - it is the handler of last resort.
   (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename))
@@ -795,21 +858,47 @@ message at DEPTH in the current thread."
     ;; Run the content handlers until one of them returns a non-nil
     ;; value.
     (while (and handlers
-               (not (funcall (car handlers) msg part content-type nth depth declared-type)))
+               (not (condition-case err
+                        (funcall (car handlers) msg part content-type nth depth declared-type)
+                      (error (progn
+                               (insert "!!! Bodypart insert error: ")
+                               (insert (error-message-string err))
+                               (insert " !!!\n") nil)))))
       (setq handlers (cdr handlers))))
   t)
 
-(defun notmuch-show-insert-bodypart (msg part depth)
-  "Insert the body part PART at depth DEPTH in the current thread."
+(defun notmuch-show-create-part-overlays (msg beg end hide)
+  "Add an overlay to the part between BEG and END"
+  (let* ((button (button-at beg))
+        (part-beg (and button (1+ (button-end button)))))
+
+    ;; If the part contains no text we do not make it toggleable. We
+    ;; also need to check that the button is a genuine part button not
+    ;; a notmuch-wash button.
+    (when (and button (/= part-beg end) (button-get button :base-label))
+      (button-put button 'overlay (make-overlay part-beg end))
+      ;; We toggle the button for hidden parts as that gets the
+      ;; button label right.
+      (save-excursion
+       (when hide
+         (notmuch-show-toggle-part-invisibility button))))))
+
+(defun notmuch-show-insert-bodypart (msg part depth &optional hide)
+  "Insert the body part PART at depth DEPTH in the current thread.
+
+If HIDE is non-nil then initially hide this part."
   (let ((content-type (downcase (plist-get part :content-type)))
-       (nth (plist-get part :id)))
-    (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type))
-  ;; Some of the body part handlers leave point somewhere up in the
-  ;; part, so we make sure that we're down at the end.
-  (goto-char (point-max))
-  ;; Ensure that the part ends with a carriage return.
-  (unless (bolp)
-    (insert "\n")))
+       (nth (plist-get part :id))
+       (beg (point)))
+
+    (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type)
+    ;; Some of the body part handlers leave point somewhere up in the
+    ;; part, so we make sure that we're down at the end.
+    (goto-char (point-max))
+    ;; Ensure that the part ends with a carriage return.
+    (unless (bolp)
+      (insert "\n"))
+    (notmuch-show-create-part-overlays msg beg (point) hide)))
 
 (defun notmuch-show-insert-body (msg body depth)
   "Insert the body BODY at depth DEPTH in the current thread."
@@ -819,7 +908,7 @@ message at DEPTH in the current thread."
   (make-symbol (concat "notmuch-show-" type)))
 
 (defun notmuch-show-strip-re (string)
-  (replace-regexp-in-string "\\([Rr]e: *\\)+" "" string))
+  (replace-regexp-in-string "^\\([Rr]e: *\\)+" "" string))
 
 (defvar notmuch-show-previous-subject "")
 (make-variable-buffer-local 'notmuch-show-previous-subject)
@@ -832,27 +921,8 @@ message at DEPTH in the current thread."
         message-start message-end
         content-start content-end
         headers-start headers-end
-        body-start body-end
-        (headers-invis-spec (notmuch-show-make-symbol "header"))
-        (message-invis-spec (notmuch-show-make-symbol "message"))
         (bare-subject (notmuch-show-strip-re (plist-get headers :Subject))))
 
-    ;; Set `buffer-invisibility-spec' to `nil' (a list), otherwise
-    ;; removing items from `buffer-invisibility-spec' (which is what
-    ;; `notmuch-show-headers-visible' and
-    ;; `notmuch-show-message-visible' do) is a no-op and has no
-    ;; effect. This caused threads with only matching messages to have
-    ;; those messages hidden initially because
-    ;; `buffer-invisibility-spec' stayed `t'.
-    ;;
-    ;; This needs to be set here (rather than just above the call to
-    ;; `notmuch-show-headers-visible') because some of the part
-    ;; rendering or body washing functions
-    ;; (e.g. `notmuch-wash-text/plain-citations') manipulate
-    ;; `buffer-invisibility-spec').
-    (when (eq buffer-invisibility-spec t)
-      (setq buffer-invisibility-spec nil))
-
     (setq message-start (point-marker))
 
     (notmuch-show-insert-headerline headers
@@ -864,9 +934,6 @@ message at DEPTH in the current thread."
 
     (setq content-start (point-marker))
 
-    (plist-put msg :headers-invis-spec headers-invis-spec)
-    (plist-put msg :message-invis-spec message-invis-spec)
-
     ;; Set `headers-start' to point after the 'Subject:' header to be
     ;; compatible with the existing implementation. This just sets it
     ;; to after the first header.
@@ -884,7 +951,6 @@ message at DEPTH in the current thread."
 
     (setq notmuch-show-previous-subject bare-subject)
 
-    (setq body-start (point-marker))
     ;; A blank line between the headers and the body.
     (insert "\n")
     (notmuch-show-insert-body msg (plist-get msg :body)
@@ -892,7 +958,6 @@ message at DEPTH in the current thread."
     ;; Ensure that the body ends with a newline.
     (unless (bolp)
       (insert "\n"))
-    (setq body-end (point-marker))
     (setq content-end (point-marker))
 
     ;; Indent according to the depth in the thread.
@@ -905,11 +970,9 @@ message at DEPTH in the current thread."
     ;; message.
     (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end))
 
-    (let ((headers-overlay (make-overlay headers-start headers-end))
-          (invis-specs (list headers-invis-spec message-invis-spec)))
-      (overlay-put headers-overlay 'invisible invis-specs)
-      (overlay-put headers-overlay 'priority 10))
-    (overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
+    ;; Create overlays used to control visibility
+    (plist-put msg :headers-overlay (make-overlay headers-start headers-end))
+    (plist-put msg :message-overlay (make-overlay headers-start content-end))
 
     (plist-put msg :depth depth)
 
@@ -958,9 +1021,9 @@ message at DEPTH in the current thread."
   "Insert the message tree TREE at depth DEPTH in the current thread."
   (let ((msg (car tree))
        (replies (cadr tree)))
-    (if (or (not notmuch-show-elide-non-matching-messages)
-           (plist-get msg :match))
-       (notmuch-show-insert-msg msg depth))
+    ;; We test whether there is a message or just some replies.
+    (when msg
+      (notmuch-show-insert-msg msg depth))
     (notmuch-show-insert-thread replies (1+ depth))))
 
 (defun notmuch-show-insert-thread (thread depth)
@@ -971,23 +1034,62 @@ message at DEPTH in the current thread."
   "Insert the forest of threads FOREST."
   (mapc (lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
 
+(defvar notmuch-id-regexp
+  (concat
+   ;; Match the id: prefix only if it begins a word (to disallow, for
+   ;; example, matching cid:).
+   "\\<id:\\("
+   ;; If the term starts with a ", then parse Xapian's quoted boolean
+   ;; term syntax, which allows for anything as long as embedded
+   ;; double quotes escaped by doubling them.  We also disallow
+   ;; newlines (which Xapian allows) to prevent runaway terms.
+   "\"\\([^\"\n]\\|\"\"\\)*\""
+   ;; Otherwise, parse Xapian's unquoted syntax, which goes up to the
+   ;; next space or ).  We disallow [.,;] as the last character
+   ;; because these are probably part of the surrounding text, and not
+   ;; part of the id.  This doesn't match single character ids; meh.
+   "\\|[^\"[:space:])][^[:space:])]*[^])[:space:].,:;?!]"
+   "\\)")
+  "The regexp used to match id: links in messages.")
+
+(defvar notmuch-mid-regexp
+  ;; goto-address-url-regexp matched cid: links, which have the same
+  ;; grammar as the message ID part of a mid: link.  Construct the
+  ;; regexp using the same technique as goto-address-url-regexp.
+  (concat "\\<mid:\\(" thing-at-point-url-path-regexp "\\)")
+  "The regexp used to match mid: links in messages.
+
+See RFC 2392.")
+
 (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."
+This also turns id:\"<message id>\"-parts and mid: links 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-show ,(match-string-no-properties 0)))
-                       'follow-link t
-                       'help-echo "Mouse-1, RET: search for this message"
-                       'face goto-address-mail-face))))
+    (let (links)
+      (goto-char start)
+      (while (re-search-forward notmuch-id-regexp end t)
+       (push (list (match-beginning 0) (match-end 0)
+                   (match-string-no-properties 0)) links))
+      (goto-char start)
+      (while (re-search-forward notmuch-mid-regexp end t)
+       (let* ((mid-cid (match-string-no-properties 1))
+              (mid (save-match-data
+                     (string-match "^[^/]*" mid-cid)
+                     (url-unhex-string (match-string 0 mid-cid)))))
+         (push (list (match-beginning 0) (match-end 0)
+                     (notmuch-id-to-query mid)) links)))
+      (dolist (link links)
+       ;; Remove the overlay created by goto-address-mode
+       (remove-overlays (first link) (second link) 'goto-address t)
+       (make-text-button (first link) (second link)
+                         'action `(lambda (arg)
+                                    (notmuch-show ,(third link)))
+                         '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)
@@ -1025,7 +1127,8 @@ function is used."
          notmuch-show-parent-buffer parent-buffer
          notmuch-show-query-context query-context)
     (notmuch-show-build-buffer)
-    (notmuch-show-goto-first-wanted-message)))
+    (notmuch-show-goto-first-wanted-message)
+    (current-buffer)))
 
 (defun notmuch-show-build-buffer ()
   (let ((inhibit-read-only t))
@@ -1041,23 +1144,25 @@ function is used."
             (args (if notmuch-show-query-context
                       (append (list "\'") basic-args
                               (list "and (" notmuch-show-query-context ")\'"))
-                    (append (list "\'") basic-args (list "\'")))))
-       (notmuch-show-insert-forest (notmuch-query-get-threads
-                                    (cons "--exclude=false" args)))
+                    (append (list "\'") basic-args (list "\'"))))
+            (cli-args (cons "--exclude=false"
+                            (when notmuch-show-elide-non-matching-messages
+                              (list "--entire-thread=false")))))
+
+       (notmuch-show-insert-forest (notmuch-query-get-threads (append cli-args args)))
        ;; If the query context reduced the results to nothing, run
        ;; the basic query.
        (when (and (eq (buffer-size) 0)
                   notmuch-show-query-context)
          (notmuch-show-insert-forest
-          (notmuch-query-get-threads
-           (cons "--exclude=false" basic-args)))))
+          (notmuch-query-get-threads (append cli-args basic-args)))))
 
       (jit-lock-register #'notmuch-show-buttonise-links)
 
-      (run-hooks 'notmuch-show-hook))
+      ;; Set the header line to the subject of the first message.
+      (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))
 
-    ;; Set the header line to the subject of the first message.
-    (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))))
+      (run-hooks 'notmuch-show-hook))))
 
 (defun notmuch-show-capture-state ()
   "Capture the state of the current buffer.
@@ -1103,6 +1208,10 @@ reset based on the original query."
   (let ((inhibit-read-only t)
        (state (unless reset-state
                 (notmuch-show-capture-state))))
+    ;; erase-buffer does not seem to remove overlays, which can lead
+    ;; to weird effects such as remaining images, so remove them
+    ;; manually.
+    (remove-overlays)
     (erase-buffer)
     (notmuch-show-build-buffer)
     (if state
@@ -1147,7 +1256,7 @@ reset based on the original query."
        (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 "h" 'notmuch-show-toggle-visibility-headers)
        (define-key map "*" 'notmuch-show-tag-all)
        (define-key map "-" 'notmuch-show-remove-tag)
        (define-key map "+" 'notmuch-show-add-tag)
@@ -1263,18 +1372,12 @@ effects."
 ;; Functions relating to the visibility of messages and their
 ;; components.
 
-(defun notmuch-show-element-visible (props visible-p spec-property)
-  (let ((spec (plist-get props spec-property)))
-    (if visible-p
-       (remove-from-invisibility-spec spec)
-      (add-to-invisibility-spec spec))))
-
 (defun notmuch-show-message-visible (props visible-p)
-  (notmuch-show-element-visible props visible-p :message-invis-spec)
+  (overlay-put (plist-get props :message-overlay) 'invisible (not visible-p))
   (notmuch-show-set-prop :message-visible visible-p props))
 
 (defun notmuch-show-headers-visible (props visible-p)
-  (notmuch-show-element-visible props visible-p :headers-invis-spec)
+  (overlay-put (plist-get props :headers-overlay) 'invisible (not visible-p))
   (notmuch-show-set-prop :headers-visible visible-p props))
 
 ;; Functions for setting and getting attributes of the current
@@ -1374,9 +1477,18 @@ current thread."
   "Are the headers of the current message visible?"
   (notmuch-show-get-prop :headers-visible))
 
-(defun notmuch-show-mark-read ()
-  "Mark the current message as read."
-  (notmuch-show-tag-message "-unread"))
+(defun notmuch-show-mark-read (&optional unread)
+  "Mark the current message as read.
+
+Mark the current message as read by applying the tag changes in
+`notmuch-show-mark-read-tags' to it (remove the \"unread\" tag by
+default). If a prefix argument is given, the message will be
+marked as unread, i.e. the tag changes in
+`notmuch-show-mark-read-tags' will be reversed."
+  (interactive "P")
+  (when notmuch-show-mark-read-tags
+    (apply 'notmuch-show-tag-message
+          (notmuch-tag-change-list notmuch-show-mark-read-tags unread))))
 
 ;; Functions for getting attributes of several messages in the current
 ;; thread.
@@ -1517,9 +1629,11 @@ thread, navigate to the next thread in the parent search buffer."
       (goto-char (point-max)))))
 
 (defun notmuch-show-previous-message ()
-  "Show the previous message."
+  "Show the previous message or the start of the current message."
   (interactive)
-  (notmuch-show-goto-message-previous)
+  (if (= (point) (notmuch-show-message-top))
+      (notmuch-show-goto-message-previous)
+    (notmuch-show-move-to-message-top))
   (notmuch-show-mark-read)
   (notmuch-show-message-adjust))
 
@@ -1579,7 +1693,9 @@ to show, nil otherwise."
 (defun notmuch-show-previous-open-message ()
   "Show the previous open message."
   (interactive)
-  (while (and (notmuch-show-goto-message-previous)
+  (while (and (if (= (point) (notmuch-show-message-top))
+                 (notmuch-show-goto-message-previous)
+               (notmuch-show-move-to-message-top))
              (not (notmuch-show-message-visible-p))))
   (notmuch-show-mark-read)
   (notmuch-show-message-adjust))
@@ -1609,7 +1725,7 @@ than only the current message."
   (let (shell-command)
     (if entire-thread
        (setq shell-command
-             (concat notmuch-command " show --format=mbox "
+             (concat notmuch-command " show --format=mbox --exclude=false "
                      (shell-quote-argument
                       (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
                      " | " command))
@@ -1673,7 +1789,7 @@ See `notmuch-tag' for information on the format of TAG-CHANGES."
   (interactive)
   (notmuch-show-tag "-"))
 
-(defun notmuch-show-toggle-headers ()
+(defun notmuch-show-toggle-visibility-headers ()
   "Toggle the visibility of the current message headers."
   (interactive)
   (let ((props (notmuch-show-get-message-properties)))
@@ -1727,18 +1843,20 @@ argument, hide all of the messages."
 (defun notmuch-show-archive-thread (&optional unarchive)
   "Archive each message in thread.
 
-Archive each message currently shown by removing the \"inbox\"
-tag from each.  If a prefix argument is given, the messages will
-be \"unarchived\" (ie. the \"inbox\" tag will be added instead of
-removed).
+Archive each message currently shown by applying the tag changes
+in `notmuch-archive-tags' to each (remove the \"inbox\" tag by
+default). If a prefix argument is given, the messages will be
+\"unarchived\", i.e. the tag changes in `notmuch-archive-tags'
+will be reversed.
 
 Note: This command is safe from any race condition of new messages
 being delivered to the same thread. It does not archive the
 entire thread, but only the messages shown in the current
 buffer."
   (interactive "P")
-  (let ((op (if unarchive "+" "-")))
-    (notmuch-show-tag-all (concat op "inbox"))))
+  (when notmuch-archive-tags
+    (notmuch-show-tag-all
+     (notmuch-tag-change-list notmuch-archive-tags unarchive))))
 
 (defun notmuch-show-archive-thread-then-next ()
   "Archive all messages in the current buffer, then show next thread from search."
@@ -1753,14 +1871,17 @@ buffer."
   (notmuch-show-next-thread))
 
 (defun notmuch-show-archive-message (&optional unarchive)
-  "Archive the current message (remove \"inbox\" tag).
+  "Archive the current message.
 
-If a prefix argument is given, the message will be
-\"unarchived\" (ie. the \"inbox\" tag will be added instead of
-removed)."
+Archive the current message by applying the tag changes in
+`notmuch-archive-tags' to it (remove the \"inbox\" tag by
+default). If a prefix argument is given, the message will be
+\"unarchived\", i.e. the tag changes in `notmuch-archive-tags'
+will be reversed."
   (interactive "P")
-  (let ((op (if unarchive "+" "-")))
-    (notmuch-show-tag-message (concat op "inbox"))))
+  (when notmuch-archive-tags
+    (apply 'notmuch-show-tag-message
+          (notmuch-tag-change-list notmuch-archive-tags unarchive))))
 
 (defun notmuch-show-archive-message-then-next-or-exit ()
   "Archive the current message, then show the next open message in the current thread.
@@ -1801,10 +1922,16 @@ thread from search."
   (interactive)
   (notmuch-common-do-stash (notmuch-show-get-from)))
 
-(defun notmuch-show-stash-message-id ()
-  "Copy id: query matching the current message to kill-ring."
-  (interactive)
-  (notmuch-common-do-stash (notmuch-show-get-message-id)))
+(defun notmuch-show-stash-message-id (&optional stash-thread-id)
+  "Copy id: query matching the current message to kill-ring.
+
+If invoked with a prefix argument (or STASH-THREAD-ID is
+non-nil), copy thread: query matching the current thread to
+kill-ring."
+  (interactive "P")
+  (if stash-thread-id
+      (notmuch-common-do-stash notmuch-show-thread-id)
+    (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."
@@ -1860,7 +1987,10 @@ the user (see `notmuch-show-stash-mlarchive-link-alist')."
 
 (defun notmuch-show-part-button-default (&optional button)
   (interactive)
-  (notmuch-show-part-button-internal button notmuch-show-part-button-default-action))
+  (let ((button (or button (button-at (point)))))
+    (if (button-get button 'overlay)
+       (notmuch-show-toggle-part-invisibility button)
+      (notmuch-show-part-button-internal button notmuch-show-part-button-default-action))))
 
 (defun notmuch-show-part-button-save (&optional button)
   (interactive)
@@ -1874,6 +2004,10 @@ the user (see `notmuch-show-stash-mlarchive-link-alist')."
   (interactive)
   (notmuch-show-part-button-internal button #'notmuch-show-interactively-view-part))
 
+(defun notmuch-show-part-button-pipe (&optional button)
+  (interactive)
+  (notmuch-show-part-button-internal button #'notmuch-show-pipe-part))
+
 (defun notmuch-show-part-button-internal (button handler)
   (let ((button (or button (button-at (point)))))
     (if button
index 0c0fc87583ad3439c04984b8879310d72062c1ed..4fce3a9873a70100d3ef833eb717241529de66d0 100644 (file)
@@ -140,6 +140,21 @@ notmuch-after-tag-hook will be run."
   ;; in all cases we return tag-changes as a list
   tag-changes)
 
+(defun notmuch-tag-change-list (tags &optional reverse)
+  "Convert TAGS into a list of tag changes.
+
+Add a \"+\" prefix to any tag in TAGS list that doesn't already
+begin with a \"+\" or a \"-\". If REVERSE is non-nil, replace all
+\"+\" prefixes with \"-\" and vice versa in the result."
+  (mapcar (lambda (str)
+           (let ((s (if (string-match "^[+-]" str) str (concat "+" str))))
+             (if reverse
+                 (concat (if (= (string-to-char s) ?-) "+" "-")
+                         (substring s 1))
+               s)))
+         tags))
+
+
 ;;
 
 (provide 'notmuch-tag)
index 56981d0635aad98913ae90e46f88f3e739fa261d..d6db4fa290970dd9bf851bd002f08f5e06fdc77d 100644 (file)
@@ -87,11 +87,19 @@ If there is one more line than the sum of
 `notmuch-wash-citation-lines-suffix', show that, otherwise
 collapse the remaining lines into a button.")
 
+(defvar notmuch-wash-wrap-lines-length nil
+  "Wrap line after at most this many characters.
+
+If this is nil, lines in messages will be wrapped to fit in the
+current window. If this is a number, lines will be wrapped after
+this many characters or at the window width (whichever one is
+lower).")
+
 (defun notmuch-wash-toggle-invisible-action (cite-button)
-  (let ((invis-spec (button-get cite-button 'invisibility-spec)))
-    (if (invisible-p invis-spec)
-       (remove-from-invisibility-spec invis-spec)
-      (add-to-invisibility-spec invis-spec)))
+  ;; Toggle overlay visibility
+  (let ((overlay (button-get cite-button 'overlay)))
+    (overlay-put overlay 'invisible (not (overlay-get overlay 'invisible))))
+  ;; Update button text
   (let* ((new-start (button-start cite-button))
         (overlay (button-get cite-button 'overlay))
         (button-label (notmuch-wash-button-label overlay))
@@ -102,9 +110,7 @@ collapse the remaining lines into a button.")
     (let ((old-end (button-end cite-button)))
       (move-overlay cite-button new-start (point))
       (delete-region (point) old-end))
-    (goto-char (min old-point (1- (button-end cite-button)))))
-  (force-window-update)
-  (redisplay t))
+    (goto-char (min old-point (1- (button-end cite-button))))))
 
 (define-button-type 'notmuch-wash-button-invisibility-toggle-type
   'action 'notmuch-wash-toggle-invisible-action
@@ -124,8 +130,8 @@ collapse the remaining lines into a button.")
   :supertype 'notmuch-wash-button-invisibility-toggle-type)
 
 (defun notmuch-wash-region-isearch-show (overlay)
-  (dolist (invis-spec (overlay-get overlay 'invisible))
-    (remove-from-invisibility-spec invis-spec)))
+  (notmuch-wash-toggle-invisible-action
+   (overlay-get overlay 'notmuch-wash-button)))
 
 (defun notmuch-wash-button-label (overlay)
   (let* ((type (overlay-get overlay 'type))
@@ -150,14 +156,10 @@ that PREFIX should not include a newline."
   ;; since the newly created symbol has no plist.
 
   (let ((overlay (make-overlay beg end))
-       (message-invis-spec (plist-get msg :message-invis-spec))
-       (invis-spec (make-symbol (concat "notmuch-" type "-region")))
        (button-type (intern-soft (concat "notmuch-wash-button-"
                                          type "-toggle-type"))))
-    (add-to-invisibility-spec invis-spec)
-    (overlay-put overlay 'invisible (list invis-spec message-invis-spec))
+    (overlay-put overlay 'invisible t)
     (overlay-put overlay 'isearch-open-invisible #'notmuch-wash-region-isearch-show)
-    (overlay-put overlay 'priority 10)
     (overlay-put overlay 'type type)
     (goto-char (1+ end))
     (save-excursion
@@ -166,10 +168,10 @@ that PREFIX should not include a newline."
          (insert-before-markers prefix))
       (let ((button-beg (point)))
        (insert-before-markers (notmuch-wash-button-label overlay) "\n")
-       (make-button button-beg (1- (point))
-                    'invisibility-spec invis-spec
-                    'overlay overlay
-                    :type button-type)))))
+       (let ((button (make-button button-beg (1- (point))
+                                  'overlay overlay
+                                  :type button-type)))
+         (overlay-put overlay 'notmuch-wash-button button))))))
 
 (defun notmuch-wash-excerpt-citations (msg depth)
   "Excerpt citations and up to one signature."
@@ -276,16 +278,24 @@ Perform several transformations on the message body:
 ;;
 
 (defun notmuch-wash-wrap-long-lines (msg depth)
-  "Wrap any long lines in the message to the width of the window.
-
-When doing so, maintaining citation leaders in the wrapped text."
-
-  (let ((coolj-wrap-follows-window-size nil)
-       (fill-column (- (window-width)
-                       depth
-                       ;; 2 to avoid poor interaction with
-                       ;; `word-wrap'.
-                       2)))
+  "Wrap long lines in the message.
+
+If `notmuch-wash-wrap-lines-length' is a number, this will wrap
+the message lines to the minimum of the width of the window or
+its value. Otherwise, this function will wrap long lines in the
+message at the window width. When doing so, citation leaders in
+the wrapped text are maintained."
+
+  (let* ((coolj-wrap-follows-window-size nil)
+        (limit (if (numberp notmuch-wash-wrap-lines-length)
+                   (min notmuch-wash-wrap-lines-length
+                        (window-width))
+                 (window-width)))
+        (fill-column (- limit
+                        depth
+                        ;; 2 to avoid poor interaction with
+                        ;; `word-wrap'.
+                        2)))
     (coolj-wrap-region (point-min) (point-max))))
 
 ;;
@@ -366,71 +376,4 @@ for error."
 
 ;;
 
-;; Temporary workaround for Emacs bug #8721
-;; http://debbugs.gnu.org/cgi/bugreport.cgi?bug=8721
-
-(defun notmuch-isearch-range-invisible (beg end)
-  "Same as `isearch-range-invisible' but with fixed Emacs bug #8721."
-  (when (/= beg end)
-    ;; Check that invisibility runs up to END.
-    (save-excursion
-      (goto-char beg)
-      (let (;; can-be-opened keeps track if we can open some overlays.
-           (can-be-opened (eq search-invisible 'open))
-           ;; the list of overlays that could be opened
-           (crt-overlays nil))
-       (when (and can-be-opened isearch-hide-immediately)
-         (isearch-close-unnecessary-overlays beg end))
-       ;; If the following character is currently invisible,
-       ;; skip all characters with that same `invisible' property value.
-       ;; Do that over and over.
-       (while (and (< (point) end) (invisible-p (point)))
-         (if (invisible-p (get-text-property (point) 'invisible))
-             (progn
-               (goto-char (next-single-property-change (point) 'invisible
-                                                       nil end))
-               ;; if text is hidden by an `invisible' text property
-               ;; we cannot open it at all.
-               (setq can-be-opened nil))
-           (when can-be-opened
-             (let ((overlays (overlays-at (point)))
-                   ov-list
-                   o
-                   invis-prop)
-               (while overlays
-                 (setq o (car overlays)
-                       invis-prop (overlay-get o 'invisible))
-                 (if (invisible-p invis-prop)
-                     (if (overlay-get o 'isearch-open-invisible)
-                         (setq ov-list (cons o ov-list))
-                       ;; We found one overlay that cannot be
-                       ;; opened, that means the whole chunk
-                       ;; cannot be opened.
-                       (setq can-be-opened nil)))
-                 (setq overlays (cdr overlays)))
-               (if can-be-opened
-                   ;; It makes sense to append to the open
-                   ;; overlays list only if we know that this is
-                   ;; t.
-                   (setq crt-overlays (append ov-list crt-overlays)))))
-           (goto-char (next-overlay-change (point)))))
-       ;; See if invisibility reaches up thru END.
-       (if (>= (point) end)
-           (if (and can-be-opened (consp crt-overlays))
-               (progn
-                 (setq isearch-opened-overlays
-                       (append isearch-opened-overlays crt-overlays))
-                 (mapc 'isearch-open-overlay-temporary crt-overlays)
-                 nil)
-             (setq isearch-hidden t)))))))
-
-(defadvice isearch-range-invisible (around notmuch-isearch-range-invisible-advice activate)
-  "Call `notmuch-isearch-range-invisible' instead of the original
-`isearch-range-invisible' when in `notmuch-show-mode' mode."
-  (if (eq major-mode 'notmuch-show-mode)
-      (setq ad-return-value (notmuch-isearch-range-invisible beg end))
-    ad-do-it))
-
-;;
-
 (provide 'notmuch-wash)
index c6236db26cf5327d8ad152ffa9363eb3dfb6ddb7..c98a4febbf69f9dcc100aa8e2b3449d77a9ed89d 100644 (file)
@@ -60,7 +60,7 @@
 (require 'notmuch-message)
 
 (defcustom notmuch-search-result-format
-  `(("date" . "%s ")
+  `(("date" . "%12s ")
     ("count" . "%-7s ")
     ("authors" . "%-20s ")
     ("subject" . "%s ")
        date, count, authors, subject, tags
 For example:
        (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\)
-                                            \(\"subject\" . \"%s\"\)\)\)"
+                                            \(\"subject\" . \"%s\"\)\)\)
+Line breaks are permitted in format strings (though this is
+currently experimental).  Note that a line break at the end of an
+\"authors\" field will get elided if the authors list is long;
+place it instead at the beginning of the following field.  To
+enter a line break when setting this variable with setq, use \\n.
+To enter a line break in customize, press \\[quoted-insert] C-j."
   :type '(alist :key-type (string) :value-type (string))
   :group 'notmuch-search)
 
@@ -287,18 +293,25 @@ For a mouse binding, return nil."
 (defun notmuch-search-next-thread ()
   "Select the next thread in the search results."
   (interactive)
-  (forward-line 1))
+  (when (notmuch-search-get-result)
+    (goto-char (notmuch-search-result-end))))
 
 (defun notmuch-search-previous-thread ()
   "Select the previous thread in the search results."
   (interactive)
-  (forward-line -1))
+  (if (notmuch-search-get-result)
+      (unless (bobp)
+       (goto-char (notmuch-search-result-beginning (- (point) 1))))
+    ;; We must be past the end; jump to the last result
+    (notmuch-search-last-thread)))
 
 (defun notmuch-search-last-thread ()
   "Select the last thread in the search results."
   (interactive)
   (goto-char (point-max))
-  (forward-line -2))
+  (forward-line -2)
+  (let ((beg (notmuch-search-result-beginning)))
+    (when beg (goto-char beg))))
 
 (defun notmuch-search-first-thread ()
   "Select the first thread in the search results."
@@ -375,8 +388,9 @@ any tags).
 Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
 keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
 is a convenience for archiving a thread (removing the \"inbox\"
-tag). The '\\[notmuch-search-tag-all]' key can be used to add or remove a tag from all
-threads in the current buffer.
+tag). The '\\[notmuch-search-tag-all]' key can be used to add and/or remove tags from all
+messages (as opposed to threads) that match the current query.  Use with caution, as this
+will also tag matching messages that arrived *after* constructing the buffer.
 
 Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
 based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
@@ -401,25 +415,78 @@ Complete list of currently available key bindings:
        mode-name "notmuch-search")
   (setq buffer-read-only t))
 
+(defun notmuch-search-get-result (&optional pos)
+  "Return the result object for the thread at POS (or point).
+
+If there is no thread at POS (or point), returns nil."
+  (get-text-property (or pos (point)) 'notmuch-search-result))
+
+(defun notmuch-search-result-beginning (&optional pos)
+  "Return the point at the beginning of the thread at POS (or point).
+
+If there is no thread at POS (or point), returns nil."
+  (when (notmuch-search-get-result pos)
+    ;; We pass 1+point because previous-single-property-change starts
+    ;; searching one before the position we give it.
+    (previous-single-property-change (1+ (or pos (point)))
+                                    'notmuch-search-result nil (point-min))))
+
+(defun notmuch-search-result-end (&optional pos)
+  "Return the point at the end of the thread at POS (or point).
+
+The returned point will be just after the newline character that
+ends the result line.  If there is no thread at POS (or point),
+returns nil"
+  (when (notmuch-search-get-result pos)
+    (next-single-property-change (or pos (point)) 'notmuch-search-result
+                                nil (point-max))))
+
+(defun notmuch-search-foreach-result (beg end function)
+  "Invoke FUNCTION for each result between BEG and END.
+
+FUNCTION should take one argument.  It will be applied to the
+character position of the beginning of each result that overlaps
+the region between points BEG and END.  As a special case, if (=
+BEG END), FUNCTION will be applied to the result containing point
+BEG."
+
+  (lexical-let ((pos (notmuch-search-result-beginning beg))
+               ;; End must be a marker in case function changes the
+               ;; text.
+               (end (copy-marker end))
+               ;; Make sure we examine at least one result, even if
+               ;; (= beg end).
+               (first t))
+    ;; We have to be careful if the region extends beyond the results.
+    ;; In this case, pos could be null or there could be no result at
+    ;; pos.
+    (while (and pos (or (< pos end) first))
+      (when (notmuch-search-get-result pos)
+       (funcall function pos))
+      (setq pos (notmuch-search-result-end pos)
+           first nil))))
+;; Unindent the function argument of notmuch-search-foreach-result so
+;; the indentation of callers doesn't get out of hand.
+(put 'notmuch-search-foreach-result 'lisp-indent-function 2)
+
 (defun notmuch-search-properties-in-region (property beg end)
-  (save-excursion
-    (let ((output nil)
-         (last-line (line-number-at-pos end))
-         (max-line (- (line-number-at-pos (point-max)) 2)))
-      (goto-char beg)
-      (beginning-of-line)
-      (while (<= (line-number-at-pos) (min last-line max-line))
-       (setq output (cons (get-text-property (point) property) output))
-       (forward-line 1))
-      output)))
-
-(defun notmuch-search-find-thread-id ()
-  "Return the thread for the current thread"
-  (get-text-property (point) 'notmuch-search-thread-id))
+  (let (output)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (push (plist-get (notmuch-search-get-result pos) property) output)))
+    output))
+
+(defun notmuch-search-find-thread-id (&optional bare)
+  "Return the thread for the current thread
+
+If BARE is set then do not prefix with \"thread:\""
+  (let ((thread (plist-get (notmuch-search-get-result) :thread)))
+    (when thread (concat (unless bare "thread:") thread))))
 
 (defun notmuch-search-find-thread-id-region (beg end)
   "Return a list of threads for the current region"
-  (notmuch-search-properties-in-region 'notmuch-search-thread-id beg end))
+  (mapcar (lambda (thread) (concat "thread:" thread))
+         (notmuch-search-properties-in-region :thread beg end)))
 
 (defun notmuch-search-find-thread-id-region-search (beg end)
   "Return a search string for threads for the current region"
@@ -427,19 +494,19 @@ Complete list of currently available key bindings:
 
 (defun notmuch-search-find-authors ()
   "Return the authors for the current thread"
-  (get-text-property (point) 'notmuch-search-authors))
+  (plist-get (notmuch-search-get-result) :authors))
 
 (defun notmuch-search-find-authors-region (beg end)
   "Return a list of authors for the current region"
-  (notmuch-search-properties-in-region 'notmuch-search-authors beg end))
+  (notmuch-search-properties-in-region :authors beg end))
 
 (defun notmuch-search-find-subject ()
   "Return the subject for the current thread"
-  (get-text-property (point) 'notmuch-search-subject))
+  (plist-get (notmuch-search-get-result) :subject))
 
 (defun notmuch-search-find-subject-region (beg end)
   "Return a list of authors for the current region"
-  (notmuch-search-properties-in-region 'notmuch-search-subject beg end))
+  (notmuch-search-properties-in-region :subject beg end))
 
 (defun notmuch-search-show-thread ()
   "Display the currently selected thread."
@@ -469,66 +536,37 @@ Complete list of currently available key bindings:
 (defun notmuch-call-notmuch-process (&rest args)
   "Synchronously invoke \"notmuch\" with the given list of arguments.
 
-Output from the process will be presented to the user as an error
-and will also appear in a buffer named \"*Notmuch errors*\"."
-  (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
-    (with-current-buffer error-buffer
-       (erase-buffer))
-    (if (eq (apply 'call-process notmuch-command nil error-buffer nil args) 0)
-       (point)
-      (progn
-       (with-current-buffer error-buffer
-         (let ((beg (point-min))
-               (end (- (point-max) 1)))
-           (error (buffer-substring beg end))
-           ))))))
-
-(defun notmuch-search-set-tags (tags)
-  (save-excursion
-    (end-of-line)
-    (re-search-backward "(")
-    (forward-char)
-    (let ((beg (point))
-         (inhibit-read-only t))
-      (re-search-forward ")")
-      (backward-char)
-      (let ((end (point)))
-       (delete-region beg end)
-       (insert (propertize (mapconcat  'identity tags " ")
-                           'face 'notmuch-tag-face))))))
-
-(defun notmuch-search-get-tags ()
-  (save-excursion
-    (end-of-line)
-    (re-search-backward "(")
-    (let ((beg (+ (point) 1)))
-      (re-search-forward ")")
-      (let ((end (- (point) 1)))
-       (split-string (buffer-substring-no-properties beg end))))))
+If notmuch exits with a non-zero status, output from the process
+will appear in a buffer named \"*Notmuch errors*\" and an error
+will be signaled."
+  (with-temp-buffer
+    (let ((status (apply #'call-process notmuch-command nil t nil args)))
+      (notmuch-check-exit-status status (cons notmuch-command args)
+                                (buffer-string)))))
+
+(defun notmuch-search-set-tags (tags &optional pos)
+  (let ((new-result (plist-put (notmuch-search-get-result pos) :tags tags)))
+    (notmuch-search-update-result new-result pos)))
+
+(defun notmuch-search-get-tags (&optional pos)
+  (plist-get (notmuch-search-get-result pos) :tags))
 
 (defun notmuch-search-get-tags-region (beg end)
-  (save-excursion
-    (let ((output nil)
-         (last-line (line-number-at-pos end))
-         (max-line (- (line-number-at-pos (point-max)) 2)))
-      (goto-char beg)
-      (while (<= (line-number-at-pos) (min last-line max-line))
-       (setq output (append output (notmuch-search-get-tags)))
-       (forward-line 1))
-      output)))
+  (let (output)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (setq output (append output (notmuch-search-get-tags pos)))))
+    output))
 
 (defun notmuch-search-tag-region (beg end &optional tag-changes)
   "Change tags for threads in the given region."
   (let ((search-string (notmuch-search-find-thread-id-region-search beg end)))
     (setq tag-changes (funcall 'notmuch-tag search-string tag-changes))
-    (save-excursion
-      (let ((last-line (line-number-at-pos end))
-           (max-line (- (line-number-at-pos (point-max)) 2)))
-       (goto-char beg)
-       (while (<= (line-number-at-pos) (min last-line max-line))
-         (notmuch-search-set-tags
-          (notmuch-update-tags (notmuch-search-get-tags) tag-changes))
-         (forward-line))))))
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (notmuch-search-set-tags
+        (notmuch-update-tags (notmuch-search-get-tags pos) tag-changes)
+        pos)))))
 
 (defun notmuch-search-tag (&optional tag-changes)
   "Change tags for the currently selected thread or region.
@@ -549,17 +587,49 @@ See `notmuch-tag' for information on the format of TAG-CHANGES."
   (interactive)
   (notmuch-search-tag "-"))
 
-(defun notmuch-search-archive-thread ()
-  "Archive the currently selected thread (remove its \"inbox\" tag).
+(defun notmuch-search-archive-thread (&optional unarchive)
+  "Archive the currently selected thread.
+
+Archive each message in the currently selected thread by applying
+the tag changes in `notmuch-archive-tags' to each (remove the
+\"inbox\" tag by default). If a prefix argument is given, the
+messages will be \"unarchived\" (i.e. the tag changes in
+`notmuch-archive-tags' will be reversed).
 
 This function advances the next thread when finished."
-  (interactive)
-  (notmuch-search-tag '("-inbox"))
+  (interactive "P")
+  (when notmuch-archive-tags
+    (notmuch-search-tag
+     (notmuch-tag-change-list notmuch-archive-tags unarchive)))
   (notmuch-search-next-thread))
 
-(defvar notmuch-search-process-filter-data nil
-  "Data that has not yet been processed.")
-(make-variable-buffer-local 'notmuch-search-process-filter-data)
+(defun notmuch-search-update-result (result &optional pos)
+  "Replace the result object of the thread at POS (or point) by
+RESULT and redraw it.
+
+This will keep point in a reasonable location.  However, if there
+are enclosing save-excursions and the saved point is in the
+result being updated, the point will be restored to the beginning
+of the result."
+  (let ((start (notmuch-search-result-beginning pos))
+       (end (notmuch-search-result-end pos))
+       (init-point (point))
+       (inhibit-read-only t))
+    ;; Delete the current thread
+    (delete-region start end)
+    ;; Insert the updated thread
+    (notmuch-search-show-result result start)
+    ;; If point was inside the old result, make an educated guess
+    ;; about where to place it now.  Unfortunately, this won't work
+    ;; with save-excursion (or any other markers that would be nice to
+    ;; preserve, such as the window start), but there's nothing we can
+    ;; do about that without a way to retrieve markers in a region.
+    (when (and (>= init-point start) (<= init-point end))
+      (let* ((new-end (notmuch-search-result-end start))
+            (new-point (if (= init-point end)
+                           new-end
+                         (min init-point (- new-end 1)))))
+       (goto-char new-point)))))
 
 (defun notmuch-search-process-sentinel (proc msg)
   "Add a message to let user know when \"notmuch search\" exits"
@@ -567,7 +637,9 @@ This function advances the next thread when finished."
        (status (process-status proc))
        (exit-status (process-exit-status proc))
        (never-found-target-thread nil))
-    (if (memq status '(exit signal))
+    (when (memq status '(exit signal))
+      (catch 'return
+       (kill-buffer (process-get proc 'parse-buf))
        (if (buffer-live-p buffer)
            (with-current-buffer buffer
              (save-excursion
@@ -577,19 +649,26 @@ This function advances the next thread when finished."
                  (if (eq status 'signal)
                      (insert "Incomplete search results (search process was killed).\n"))
                  (when (eq status 'exit)
-                   (if notmuch-search-process-filter-data
-                       (insert (concat "Error: Unexpected output from notmuch search:\n" notmuch-search-process-filter-data)))
-                   (insert "End of search results.")
-                   (unless (= exit-status 0)
-                     (insert (format " (process returned %d)" exit-status)))
-                   (insert "\n")
+                   (insert "End of search results.\n")
+                   ;; For version mismatch, there's no point in
+                   ;; showing the search buffer
+                   (when (or (= exit-status 20) (= exit-status 21))
+                     (kill-buffer))
+                   (condition-case err
+                       (notmuch-check-async-exit-status proc msg)
+                     ;; Suppress the error signal since strange
+                     ;; things happen if a sentinel signals.  Mimic
+                     ;; the top-level's handling of error messages.
+                     (error
+                      (message "%s" (second err))
+                      (throw 'return nil)))
                    (if (and atbob
                             (not (string= notmuch-search-target-thread "found")))
                        (set 'never-found-target-thread t)))))
              (when (and never-found-target-thread
                       notmuch-search-target-line)
                  (goto-char (point-min))
-                 (forward-line (1- notmuch-search-target-line))))))))
+                 (forward-line (1- notmuch-search-target-line)))))))))
 
 (defcustom notmuch-search-line-faces '(("unread" :weight bold)
                                       ("flagged" :foreground "blue"))
@@ -612,20 +691,13 @@ foreground and blue background."
 
 (defun notmuch-search-color-line (start end line-tag-list)
   "Colorize lines in `notmuch-show' based on tags."
-  ;; Create the overlay only if the message has tags which match one
-  ;; of those specified in `notmuch-search-line-faces'.
-  (let (overlay)
-    (mapc (lambda (elem)
-           (let ((tag (car elem))
-                 (attributes (cdr elem)))
-             (when (member tag line-tag-list)
-               (when (not overlay)
-                 (setq overlay (make-overlay start end)))
-               ;; Merge the specified properties with any already
-               ;; applied from an earlier match.
-               (overlay-put overlay 'face
-                            (append (overlay-get overlay 'face) attributes)))))
-         notmuch-search-line-faces)))
+  (mapc (lambda (elem)
+         (let ((tag (car elem))
+               (attributes (cdr elem)))
+           (when (member tag line-tag-list)
+             (notmuch-combine-face-text-property start end attributes))))
+       ;; Reverse the list so earlier entries take precedence
+       (reverse notmuch-search-line-faces)))
 
 (defun notmuch-search-author-propertize (authors)
   "Split `authors' into matching and non-matching authors and
@@ -707,78 +779,67 @@ non-authors is found, assume that all of the authors match."
          (overlay-put overlay 'isearch-open-invisible #'delete-overlay)))
       (insert padding))))
 
-(defun notmuch-search-insert-field (field date count authors subject tags)
+(defun notmuch-search-insert-field (field format-string result)
   (cond
    ((string-equal field "date")
-    (insert (propertize (format (cdr (assoc field notmuch-search-result-format)) date)
+    (insert (propertize (format format-string (plist-get result :date_relative))
                        'face 'notmuch-search-date)))
    ((string-equal field "count")
-    (insert (propertize (format (cdr (assoc field notmuch-search-result-format)) count)
+    (insert (propertize (format format-string
+                               (format "[%s/%s]" (plist-get result :matched)
+                                       (plist-get result :total)))
                        'face 'notmuch-search-count)))
    ((string-equal field "subject")
-    (insert (propertize (format (cdr (assoc field notmuch-search-result-format)) subject)
+    (insert (propertize (format format-string (plist-get result :subject))
                        'face 'notmuch-search-subject)))
 
    ((string-equal field "authors")
-    (notmuch-search-insert-authors (cdr (assoc field notmuch-search-result-format)) authors))
+    (notmuch-search-insert-authors format-string (plist-get result :authors)))
 
    ((string-equal field "tags")
-    (insert (concat "(" (propertize tags 'font-lock-face 'notmuch-tag-face) ")")))))
-
-(defun notmuch-search-show-result (date count authors subject tags)
-  (let ((fields) (field))
-    (setq fields (mapcar 'car notmuch-search-result-format))
-    (loop for field in fields
-         do (notmuch-search-insert-field field date count authors subject tags)))
-  (insert "\n"))
+    (let ((tags-str (mapconcat 'identity (plist-get result :tags) " ")))
+      (insert (propertize (format format-string tags-str)
+                         'face 'notmuch-tag-face))))))
+
+(defun notmuch-search-show-result (result &optional pos)
+  "Insert RESULT at POS or the end of the buffer if POS is null."
+  ;; Ignore excluded matches
+  (unless (= (plist-get result :matched) 0)
+    (let ((beg (or pos (point-max))))
+      (save-excursion
+       (goto-char beg)
+       (dolist (spec notmuch-search-result-format)
+         (notmuch-search-insert-field (car spec) (cdr spec) result))
+       (insert "\n")
+       (notmuch-search-color-line beg (point) (plist-get result :tags))
+       (put-text-property beg (point) 'notmuch-search-result result))
+      (when (string= (plist-get result :thread) notmuch-search-target-thread)
+       (setq notmuch-search-target-thread "found")
+       (goto-char beg)))))
+
+(defun notmuch-search-show-error (string &rest objects)
+  (save-excursion
+    (goto-char (point-max))
+    (insert "Error: Unexpected output from notmuch search:\n")
+    (insert (apply #'format string objects))
+    (insert "\n")))
 
 (defun notmuch-search-process-filter (proc string)
   "Process and filter the output of \"notmuch search\""
-  (let ((buffer (process-buffer proc))
-       (found-target nil))
-    (if (buffer-live-p buffer)
-       (with-current-buffer buffer
-         (save-excursion
-           (let ((line 0)
-                 (more t)
-                 (inhibit-read-only t)
-                 (string (concat notmuch-search-process-filter-data string)))
-             (setq notmuch-search-process-filter-data nil)
-             (while more
-               (while (and (< line (length string)) (= (elt string line) ?\n))
-                 (setq line (1+ line)))
-               (if (string-match "^\\(thread:[0-9A-Fa-f]*\\) \\([^][]*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
-                   (let* ((thread-id (match-string 1 string))
-                          (date (match-string 2 string))
-                          (count (match-string 3 string))
-                          (authors (match-string 4 string))
-                          (subject (match-string 5 string))
-                          (tags (match-string 6 string))
-                          (tag-list (if tags (save-match-data (split-string tags)))))
-                     (goto-char (point-max))
-                     (if (/= (match-beginning 1) line)
-                         (insert (concat "Error: Unexpected output from notmuch search:\n" (substring string line (match-beginning 1)) "\n")))
-                     ;; We currently just throw away excluded matches.
-                     (unless (eq (aref count 1) ?0)
-                       (let ((beg (point)))
-                         (notmuch-search-show-result date count authors subject tags)
-                         (notmuch-search-color-line beg (point) tag-list)
-                         (put-text-property beg (point) 'notmuch-search-thread-id thread-id)
-                         (put-text-property beg (point) 'notmuch-search-authors authors)
-                         (put-text-property beg (point) 'notmuch-search-subject subject)
-                         (when (string= thread-id notmuch-search-target-thread)
-                           (set 'found-target beg)
-                           (set 'notmuch-search-target-thread "found"))))
-                     (set 'line (match-end 0)))
-                 (set 'more nil)
-                 (while (and (< line (length string)) (= (elt string line) ?\n))
-                   (setq line (1+ line)))
-                 (if (< line (length string))
-                     (setq notmuch-search-process-filter-data (substring string line)))
-                 ))))
-         (if found-target
-             (goto-char found-target)))
-      (delete-process proc))))
+  (let ((results-buf (process-buffer proc))
+       (parse-buf (process-get proc 'parse-buf))
+       (inhibit-read-only t)
+       done)
+    (if (not (buffer-live-p results-buf))
+       (delete-process proc)
+      (with-current-buffer parse-buf
+       ;; Insert new data
+       (save-excursion
+         (goto-char (point-max))
+         (insert string))
+       (notmuch-json-parse-partial-list 'notmuch-search-show-result
+                                        'notmuch-search-show-error
+                                        results-buf)))))
 
 (defun notmuch-search-tag-all (&optional tag-changes)
   "Add/remove tags from all messages in current search buffer.
@@ -823,7 +884,7 @@ PROMPT is the string to prompt with."
        (append (list "folder:" "thread:" "id:" "date:" "from:" "to:"
                      "subject:" "attachment:")
                (mapcar (lambda (tag)
-                         (concat "tag:" tag))
+                         (concat "tag:" (notmuch-escape-boolean-term tag)))
                        (process-lines notmuch-command "search" "--output=tags" "*")))))
     (let ((keymap (copy-keymap minibuffer-local-map))
          (minibuffer-completion-table
@@ -853,7 +914,7 @@ If `query' is nil, it is read interactively from the minibuffer.
 Other optional parameters are used as follows:
 
   oldest-first: A Boolean controlling the sort order of returned threads
-  target-thread: A thread ID (with the thread: prefix) that will be made
+  target-thread: A thread ID (without the thread: prefix) that will be made
                  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."
@@ -881,10 +942,16 @@ Other optional parameters are used as follows:
        (let ((proc (start-process
                     "notmuch-search" buffer
                     notmuch-command "search"
+                    "--format=json" "--format-version=1"
                     (if oldest-first
                         "--sort=oldest-first"
                       "--sort=newest-first")
-                    query)))
+                    query))
+             ;; Use a scratch buffer to accumulate partial output.
+             ;; This buffer will be killed by the sentinel, which
+             ;; should be called no matter how the process dies.
+             (parse-buf (generate-new-buffer " *notmuch search parse*")))
+         (process-put proc 'parse-buf parse-buf)
          (set-process-sentinel proc 'notmuch-search-process-sentinel)
          (set-process-filter proc 'notmuch-search-process-filter)
          (set-process-query-on-exit-flag proc nil))))
@@ -901,7 +968,7 @@ same relative position within the new buffer."
   (interactive)
   (let ((target-line (line-number-at-pos))
        (oldest-first notmuch-search-oldest-first)
-       (target-thread (notmuch-search-find-thread-id))
+       (target-thread (notmuch-search-find-thread-id 'bare))
        (query notmuch-search-query-string)
        (continuation notmuch-search-continuation))
     (notmuch-kill-this-buffer)
diff --git a/json.c b/json.c
deleted file mode 100644 (file)
index 817fc83..0000000
--- a/json.c
+++ /dev/null
@@ -1,109 +0,0 @@
-/* notmuch - Not much of an email program, (just index and search)
- *
- * Copyright © 2009 Dave Gamble
- * Copyright © 2009 Scott Robinson
- *
- * 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/ .
- *
- * Authors: Dave Gamble
- *          Scott Robinson <scott@quadhome.com>
- *
- */
-
-#include "notmuch-client.h"
-
-/* This function was derived from the print_string_ptr function of
- * cJSON (http://cjson.sourceforge.net/) and is used by permission of
- * the following license:
- *
- * Copyright (c) 2009 Dave Gamble
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-char *
-json_quote_chararray(const void *ctx, const char *str, const size_t len)
-{
-    const char *ptr;
-    char *ptr2;
-    char *out;
-    size_t loop;
-    size_t required;
-
-    for (loop = 0, required = 0, ptr = str;
-        loop < len;
-        loop++, required++, ptr++) {
-       if ((unsigned char)(*ptr) < 32 || *ptr == '\"' || *ptr == '\\')
-           required++;
-    }
-
-    /*
-     * + 3 for:
-     * - leading quotation mark,
-     * - trailing quotation mark,
-     * - trailing NULL.
-     */
-    out = talloc_array (ctx, char, required + 3);
-
-    ptr = str;
-    ptr2 = out;
-
-    *ptr2++ = '\"';
-    for (loop = 0; loop < len; loop++) {
-       if ((unsigned char)(*ptr) > 31 && *ptr != '\"' && *ptr != '\\') {
-               *ptr2++ = *ptr++;
-           } else {
-               *ptr2++ = '\\';
-               switch (*ptr++) {
-                   case '\"':  *ptr2++ = '\"'; break;
-                   case '\\':  *ptr2++ = '\\'; break;
-                   case '\b':  *ptr2++ = 'b';  break;
-                   case '\f':  *ptr2++ = 'f';  break;
-                   case '\n':  *ptr2++ = 'n';  break;
-                   case '\r':  *ptr2++ = 'r';  break;
-                   case '\t':  *ptr2++ = 't';  break;
-                   default:     ptr2--;        break;
-               }
-           }
-    }
-    *ptr2++ = '\"';
-    *ptr2++ = '\0';
-
-    return out;
-}
-
-char *
-json_quote_str(const void *ctx, const char *str)
-{
-    if (str == NULL)
-       str = "";
-
-    return (json_quote_chararray (ctx, str, strlen (str)));
-}
index 8a9aa28a13365a783e68e811c972f611d73b6708..778594472fb52a4925b58b6fdf9b9d6ae0317f6d 100644 (file)
@@ -31,6 +31,9 @@ LINKER_NAME = libnotmuch.$(LIBRARY_SUFFIX)
 SONAME = $(LINKER_NAME).$(LIBNOTMUCH_VERSION_MAJOR)
 LIBNAME = $(SONAME).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE)
 LIBRARY_LINK_FLAG = -shared -Wl,--version-script=notmuch.sym,-soname=$(SONAME) -Wl,--no-undefined
+ifeq ($(PLATFORM),OPENBSD)
+LIBRARY_LINK_FLAG += -lc
+endif
 ifeq ($(LIBDIR_IN_LDCONFIG),1)
 ifeq ($(DESTDIR),)
 LIBRARY_INSTALL_POST_COMMAND=ldconfig
@@ -58,6 +61,7 @@ libnotmuch_c_srcs =           \
 
 libnotmuch_cxx_srcs =          \
        $(dir)/database.cc      \
+       $(dir)/parse-time-vrp.cc        \
        $(dir)/directory.cc     \
        $(dir)/index.cc         \
        $(dir)/message.cc       \
@@ -70,7 +74,7 @@ $(dir)/libnotmuch.a: $(libnotmuch_modules)
        $(call quiet,AR) rcs $@ $^
 
 $(dir)/$(LIBNAME): $(libnotmuch_modules) notmuch.sym
-       $(call quiet,CXX $(CXXFLAGS)) $(libnotmuch_modules) $(FINAL_LIBNOTMUCH_LDFLAGS) $(LIBRARY_LINK_FLAG) -o $@ util/libutil.a
+       $(call quiet,CXX $(CXXFLAGS)) $(libnotmuch_modules) $(FINAL_LIBNOTMUCH_LDFLAGS) $(LIBRARY_LINK_FLAG) -o $@ util/libutil.a parse-time-string/libparse-time-string.a
 
 notmuch.sym: $(srcdir)/$(dir)/notmuch.h $(libnotmuch_modules)
        sh $(srcdir)/$(lib)/gen-version-script.sh $< $(libnotmuch_modules) > $@
index 88532d511a8e395b7c5aa475ad7a403a6722777d..d3e65fd64a8ba0508a59e374a2daab80f987a434 100644 (file)
@@ -52,6 +52,7 @@ struct _notmuch_database {
     Xapian::QueryParser *query_parser;
     Xapian::TermGenerator *term_gen;
     Xapian::ValueRangeProcessor *value_range_processor;
+    Xapian::ValueRangeProcessor *date_range_processor;
 };
 
 /* Return the list of terms from the given iterator matching a prefix.
index 761dc1a24c3a26247dcfa7734ceb7141f02df36a..91d43298f3795a30aa833196c2f196e49382d671 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 #include "database-private.h"
+#include "parse-time-vrp.h"
 
 #include <iostream>
 
@@ -710,12 +711,14 @@ notmuch_database_open (const char *path,
        notmuch->term_gen = new Xapian::TermGenerator;
        notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
        notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+       notmuch->date_range_processor = new ParseTimeValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
 
        notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
        notmuch->query_parser->set_database (*notmuch->xapian_db);
        notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
        notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
        notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
+       notmuch->query_parser->add_valuerangeprocessor (notmuch->date_range_processor);
 
        for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
            prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
@@ -778,6 +781,8 @@ notmuch_database_close (notmuch_database_t *notmuch)
     notmuch->xapian_db = NULL;
     delete notmuch->value_range_processor;
     notmuch->value_range_processor = NULL;
+    delete notmuch->date_range_processor;
+    notmuch->date_range_processor = NULL;
 }
 
 void
@@ -1816,7 +1821,9 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
            date = notmuch_message_file_get_header (message_file, "date");
            _notmuch_message_set_header_values (message, date, from, subject);
 
-           _notmuch_message_index_file (message, filename);
+           ret = _notmuch_message_index_file (message, filename);
+           if (ret)
+               goto DONE;
        } else {
            ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
        }
index f1ea243012095d19eccf334e54e02ade3b59c089..4f7c0d85b1c350d5977a1ddd00c5e0c01665f49d 100644 (file)
@@ -54,7 +54,7 @@ notmuch_filenames_valid (notmuch_filenames_t *filenames)
 const char *
 notmuch_filenames_get (notmuch_filenames_t *filenames)
 {
-    if (filenames->iterator == NULL)
+    if ((filenames == NULL) || (filenames->iterator == NULL))
        return NULL;
 
     return filenames->iterator->string;
@@ -63,7 +63,7 @@ notmuch_filenames_get (notmuch_filenames_t *filenames)
 void
 notmuch_filenames_move_to_next (notmuch_filenames_t *filenames)
 {
-    if (filenames->iterator == NULL)
+    if ((filenames == NULL) || (filenames->iterator == NULL))
        return;
 
     filenames->iterator = filenames->iterator->next;
index e377732220effdcb537934bf6036d01528829db3..a2edd6d9b6c0795fbdb2512907265000bb54fe21 100644 (file)
@@ -435,6 +435,9 @@ _notmuch_message_index_file (notmuch_message_t *message,
     const char *from, *subject;
     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
     static int initialized = 0;
+    char from_buf[5];
+    bool is_mbox = false;
+    static bool mbox_warning = false;
 
     if (! initialized) {
        g_mime_init (0);
@@ -448,20 +451,51 @@ _notmuch_message_index_file (notmuch_message_t *message,
        goto DONE;
     }
 
+    /* Is this mbox? */
+    if (fread (from_buf, sizeof (from_buf), 1, file) == 1 &&
+       strncmp (from_buf, "From ", 5) == 0)
+       is_mbox = true;
+    rewind (file);
+
     /* Evil GMime steals my FILE* here so I won't fclose it. */
     stream = g_mime_stream_file_new (file);
 
     parser = g_mime_parser_new_with_stream (stream);
+    g_mime_parser_set_scan_from (parser, is_mbox);
 
     mime_message = g_mime_parser_construct_message (parser);
 
+    if (is_mbox) {
+       if (!g_mime_parser_eos (parser)) {
+           /* This is a multi-message mbox. */
+           ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
+           goto DONE;
+       }
+       /* For historical reasons, we support single-message mboxes,
+        * but this behavior is likely to change in the future, so
+        * warn. */
+       if (!mbox_warning) {
+           mbox_warning = true;
+           fprintf (stderr, "\
+Warning: %s is an mbox containing a single message,\n\
+likely caused by misconfigured mail delivery.  Support for single-message\n\
+mboxes is deprecated and may be removed in the future.\n", filename);
+       }
+    }
+
     from = g_mime_message_get_sender (mime_message);
-    addresses = internet_address_list_parse_string (from);
 
-    _index_address_list (message, "from", addresses);
+    addresses = internet_address_list_parse_string (from);
+    if (addresses) {
+       _index_address_list (message, "from", addresses);
+       g_object_unref (addresses);
+    }
 
     addresses = g_mime_message_get_all_recipients (mime_message);
-    _index_address_list (message, "to", addresses);
+    if (addresses) {
+       _index_address_list (message, "to", addresses);
+       g_object_unref (addresses);
+    }
 
     subject = g_mime_message_get_subject (mime_message);
     _notmuch_message_gen_terms (message, "subject", subject);
index 915aba8bf97bb1b1e1251c3a3c4a161d81912bd8..4d9af89fe44dd5923cee743572149f4cf2c22f46 100644 (file)
@@ -111,7 +111,7 @@ _notmuch_message_file_open_ctx (void *ctx, const char *filename)
     message->headers = g_hash_table_new_full (strcase_hash,
                                              strcase_equal,
                                              free,
-                                             free);
+                                             g_free);
 
     message->parsing_started = 0;
     message->parsing_finished = 0;
@@ -337,11 +337,11 @@ notmuch_message_file_get_header (notmuch_message_file_t *message,
                /* we need to add the header to those we already collected */
                newhdr = strlen(decoded_value);
                hdrsofar = strlen(header_sofar);
-               combined_header = xmalloc(hdrsofar + newhdr + 2);
+               combined_header = g_malloc(hdrsofar + newhdr + 2);
                strncpy(combined_header,header_sofar,hdrsofar);
                *(combined_header+hdrsofar) = ' ';
                strncpy(combined_header+hdrsofar+1,decoded_value,newhdr+1);
-               free (decoded_value);
+               g_free (decoded_value);
                g_hash_table_insert (message->headers, header, combined_header);
            }
        } else {
@@ -350,7 +350,7 @@ notmuch_message_file_get_header (notmuch_message_file_t *message,
                g_hash_table_insert (message->headers, header, decoded_value);
            } else {
                free (header);
-               free (decoded_value);
+               g_free (decoded_value);
                decoded_value = header_sofar;
            }
        }
index 67875065f5c93f01edbad2387172d3767be59364..320901f77eb010a1cab4502c79db6faccbc6d49c 100644 (file)
@@ -788,7 +788,9 @@ notmuch_message_get_tags (notmuch_message_t *message)
      * possible to modify the message tags (which talloc_unlink's the
      * current list from the message) while still iterating because
      * the iterator will keep the current list alive. */
-    talloc_reference (message, message->tag_list);
+    if (!talloc_reference (message, message->tag_list))
+       return NULL;
+
     return tags;
 }
 
@@ -1027,13 +1029,54 @@ notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
     return NOTMUCH_STATUS_SUCCESS;
 }
 
+/* Is the given filename within a maildir directory?
+ *
+ * Specifically, is the final directory component of 'filename' either
+ * "cur" or "new". If so, return a pointer to that final directory
+ * component within 'filename'. If not, return NULL.
+ *
+ * A non-NULL return value is guaranteed to be a valid string pointer
+ * pointing to the characters "new/" or "cur/", (but not
+ * NUL-terminated).
+ */
+static const char *
+_filename_is_in_maildir (const char *filename)
+{
+    const char *slash, *dir = NULL;
+
+    /* Find the last '/' separating directory from filename. */
+    slash = strrchr (filename, '/');
+    if (slash == NULL)
+       return NULL;
+
+    /* Jump back 4 characters to where the previous '/' will be if the
+     * directory is named "cur" or "new". */
+    if (slash - filename < 4)
+       return NULL;
+
+    slash -= 4;
+
+    if (*slash != '/')
+       return NULL;
+
+    dir = slash + 1;
+
+    if (STRNCMP_LITERAL (dir, "cur/") == 0 ||
+       STRNCMP_LITERAL (dir, "new/") == 0)
+    {
+       return dir;
+    }
+
+    return NULL;
+}
+
 notmuch_status_t
 notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
 {
     const char *flags;
     notmuch_status_t status;
     notmuch_filenames_t *filenames;
-    const char *filename;
+    const char *filename, *dir;
     char *combined_flags = talloc_strdup (message, "");
     unsigned i;
     int seen_maildir_info = 0;
@@ -1043,15 +1086,25 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
         notmuch_filenames_move_to_next (filenames))
     {
        filename = notmuch_filenames_get (filenames);
+       dir = _filename_is_in_maildir (filename);
 
-       flags = strstr (filename, ":2,");
-       if (! flags)
+       if (! dir)
            continue;
 
-       seen_maildir_info = 1;
-       flags += 3;
-
-       combined_flags = talloc_strdup_append (combined_flags, flags);
+       flags = strstr (filename, ":2,");
+       if (flags) {
+           seen_maildir_info = 1;
+           flags += 3;
+           combined_flags = talloc_strdup_append (combined_flags, flags);
+       } else if (STRNCMP_LITERAL (dir, "new/") == 0) {
+           /* Messages are delivered to new/ with no "info" part, but
+            * they effectively have default maildir flags.  According
+            * to the spec, we should ignore the info part for
+            * messages in new/, but some MUAs (mutt) can set maildir
+            * flags on messages in new/, so we're liberal in what we
+            * accept. */
+           seen_maildir_info = 1;
+       }
     }
 
     /* If none of the filenames have any maildir info field (not even
@@ -1083,47 +1136,6 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
     return status;
 }
 
-/* Is the given filename within a maildir directory?
- *
- * Specifically, is the final directory component of 'filename' either
- * "cur" or "new". If so, return a pointer to that final directory
- * component within 'filename'. If not, return NULL.
- *
- * A non-NULL return value is guaranteed to be a valid string pointer
- * pointing to the characters "new/" or "cur/", (but not
- * NUL-terminated).
- */
-static const char *
-_filename_is_in_maildir (const char *filename)
-{
-    const char *slash, *dir = NULL;
-
-    /* Find the last '/' separating directory from filename. */
-    slash = strrchr (filename, '/');
-    if (slash == NULL)
-       return NULL;
-
-    /* Jump back 4 characters to where the previous '/' will be if the
-     * directory is named "cur" or "new". */
-    if (slash - filename < 4)
-       return NULL;
-
-    slash -= 4;
-
-    if (*slash != '/')
-       return NULL;
-
-    dir = slash + 1;
-
-    if (STRNCMP_LITERAL (dir, "cur/") == 0 ||
-       STRNCMP_LITERAL (dir, "new/") == 0)
-    {
-       return dir;
-    }
-
-    return NULL;
-}
-
 /* From the set of tags on 'message' and the flag2tag table, compute a
  * set of maildir-flag actions to be taken, (flags that should be
  * either set or cleared).
index bfb41116adcff104edadf639cd95513b225289bb..7a409f54ca5a08b4de81b39cb652984c330ae758 100644 (file)
@@ -136,13 +136,17 @@ typedef enum _notmuch_private_status {
  * to or greater than NOTMUCH_STATUS_LAST_STATUS. (The idea here is
  * that the caller has previously handled any expected
  * notmuch_private_status_t values.)
+ *
+ * Note that the function _internal_error does not return. Evaluating
+ * to NOTMUCH_STATUS_SUCCESS is done purely to appease the compiler.
  */
 #define COERCE_STATUS(private_status, format, ...)                     \
     ((private_status >= (notmuch_private_status_t) NOTMUCH_STATUS_LAST_STATUS)\
      ?                                                                 \
-     (notmuch_status_t) _internal_error (format " (%s).\n",            \
-                                         ##__VA_ARGS__,                        \
-                                         __location__)                 \
+     _internal_error (format " (%s).\n",                               \
+                      ##__VA_ARGS__,                                   \
+                      __location__),                                   \
+     (notmuch_status_t) NOTMUCH_PRIVATE_STATUS_SUCCESS                 \
      :                                                                 \
      (notmuch_status_t) private_status)
 
diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc
new file mode 100644 (file)
index 0000000..33f07db
--- /dev/null
@@ -0,0 +1,61 @@
+/* parse-time-vrp.cc - date range query glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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: Jani Nikula <jani@nikula.org>
+ */
+
+#include "database-private.h"
+#include "parse-time-vrp.h"
+#include "parse-time-string.h"
+
+#define PREFIX "date:"
+
+/* See *ValueRangeProcessor in xapian-core/api/valuerangeproc.cc */
+Xapian::valueno
+ParseTimeValueRangeProcessor::operator() (std::string &begin, std::string &end)
+{
+    time_t t, now;
+
+    /* Require date: prefix in start of the range... */
+    if (STRNCMP_LITERAL (begin.c_str (), PREFIX))
+       return Xapian::BAD_VALUENO;
+
+    /* ...and remove it. */
+    begin.erase (0, sizeof (PREFIX) - 1);
+
+    /* Use the same 'now' for begin and end. */
+    if (time (&now) == (time_t) -1)
+       return Xapian::BAD_VALUENO;
+
+    if (!begin.empty ()) {
+       if (parse_time_string (begin.c_str (), &t, &now, PARSE_TIME_ROUND_DOWN))
+           return Xapian::BAD_VALUENO;
+
+       begin.assign (Xapian::sortable_serialise ((double) t));
+    }
+
+    if (!end.empty ()) {
+       if (parse_time_string (end.c_str (), &t, &now, PARSE_TIME_ROUND_UP_INCLUSIVE))
+           return Xapian::BAD_VALUENO;
+
+       end.assign (Xapian::sortable_serialise ((double) t));
+    }
+
+    return valno;
+}
diff --git a/lib/parse-time-vrp.h b/lib/parse-time-vrp.h
new file mode 100644 (file)
index 0000000..094c4f8
--- /dev/null
@@ -0,0 +1,40 @@
+/* parse-time-vrp.h - date range query glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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: Jani Nikula <jani@nikula.org>
+ */
+
+#ifndef NOTMUCH_PARSE_TIME_VRP_H
+#define NOTMUCH_PARSE_TIME_VRP_H
+
+#include <xapian.h>
+
+/* see *ValueRangeProcessor in xapian-core/include/xapian/queryparser.h */
+class ParseTimeValueRangeProcessor : public Xapian::ValueRangeProcessor {
+protected:
+    Xapian::valueno valno;
+
+public:
+    ParseTimeValueRangeProcessor (Xapian::valueno slot_)
+       : valno(slot_) { }
+
+    Xapian::valueno operator() (std::string &begin, std::string &end);
+};
+
+#endif /* NOTMUCH_PARSE_TIME_VRP_H */
index c58924f87b56de56a7799dd23aadcef35056d15d..b7e5602cfbac9a33637f310c2c3498cc42851ef1 100644 (file)
@@ -40,7 +40,7 @@ _notmuch_tags_create (const void *ctx, notmuch_string_list_t *list)
        return NULL;
 
     tags->iterator = list->head;
-    talloc_steal (tags, list);
+    (void) talloc_steal (tags, list);
 
     return tags;
 }
index d43a949cf4454ab176c92ad1360c187a2a768a3e..72e2a18a81136c0b525c667b83908710cf99ce2f 100644 (file)
@@ -32,7 +32,7 @@ COMPRESSED_MAN := $(MAN1_GZ) $(MAN5_GZ) $(MAN7_GZ)
 %.gz: %
        gzip --stdout $^ > $@
 
-.PHONY: install-man update-man-versions verify-version-manpage
+.PHONY: install-man update-man-versions
 
 install-man: $(COMPRESSED_MAN)
        mkdir -p "$(DESTDIR)$(mandir)/man1"
@@ -43,13 +43,6 @@ install-man: $(COMPRESSED_MAN)
        install -m0644 $(MAN7_GZ) $(DESTDIR)/$(mandir)/man7
        cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch.1.gz notmuch-setup.1.gz
 
-verify-version-manpage: verify-version-components
-       @echo -n "Checking that manual page version is $(VERSION)..."
-       @[ "$(VERSION)" = $$(sed -n '/^[.]TH NOTMUCH 1/{s/.*"Notmuch //;s/".*//p;}' $(MAIN_PAGE)) ] || \
-               (echo "No." && \
-                echo "Please edit version and notmuch.1 to have consistent versions." && false)
-       @echo "Good."
-
 update-man-versions: $(MAN_SOURCE)
        for file in $(MAN_SOURCE); do \
            cp $$file $$file.bak ; \
index 4f7985c37da65418a754e6be03ad3eb4fdb3beb5..6204a5944e3dc92940f401a61f9ef541d1ccc3a4 100644 (file)
@@ -1,6 +1,6 @@
-.TH NOTMUCH-CONFIG 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-CONFIG 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-config \- Access notmuch configuration file.
+notmuch-config \- access notmuch configuration file
 .SH SYNOPSIS
 
 .B notmuch config get
index 8029174c1dbc45dd2cc38848fe61c6bfd6f4c7e2..7f8bbac71ba23d5afdcfb62bfac0eeebe5e2f146 100644 (file)
@@ -1,6 +1,6 @@
-.TH NOTMUCH-COUNT 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-COUNT 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-count \- Count messages matching the given search terms.
+notmuch-count \- count messages matching the given search terms
 .SH SYNOPSIS
 
 .B notmuch count
index 9c7dd84b593a54bec43cd7551d2217363c8d5388..799fd7b64bc5ff80d6d6f7359eb7517800b8909b 100644 (file)
@@ -1,25 +1,86 @@
-.TH NOTMUCH-DUMP 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-DUMP 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-dump \- Creates a plain-text dump of the tags of each message.
+notmuch-dump \- creates a plain-text dump of the tags of each message
 
 .SH SYNOPSIS
 
 .B "notmuch dump"
-.RI "[ <" filename "> ] [--]"
+.RB  [ "\-\-format=(sup|batch-tag)"  "] [--]"
+.RI "[ --output=<" filename "> ] [--]"
 .RI "[ <" search-term ">...]"
 
 .SH DESCRIPTION
 
 Dump tags for messages matching the given search terms.
 
-Output is to the given filename, if any, or to stdout.  Note that
-using the filename argument is deprecated.
+Output is to the given filename, if any, or to stdout.
 
 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.)
 
+.TP 4
+.B \-\-format=(sup|batch-tag)
+
+Notmuch restore supports two plain text dump formats, both with one message-id
+per line, followed by a list of tags.
+
+.RS 4
+.TP 4
+.B sup
+
+The
+.B sup
+dump file format is specifically chosen to be
+compatible with the format of files produced by sup-dump.
+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).
+Each line has the following form
+
+.RS 4
+.RI < message-id >
+.B (
+.RI < tag "> ..."
+.B )
+
+with zero or more tags are separated by spaces. Note that (malformed)
+message-ids may contain arbitrary non-null characters. Note also
+that tags with spaces will not be correctly restored with this format.
+
+.RE
+
+.RE
+.RS 4
+.TP 4
+.B batch-tag
+
+The
+.B batch-tag
+dump format is intended to more robust against malformed message-ids
+and tags containing whitespace or non-\fBascii\fR(7) characters.
+Each line has the form
+
+.RS 4
+.RI "+<" "encoded-tag" "> " "" "+<" "encoded-tag" "> ... -- " "" " id:<" quoted-message-id >
+
+Tags are hex-encoded by replacing every byte not matching the regex
+.B [A-Za-z0-9@=.,_+-]
+with
+.B %nn
+where nn is the two digit hex encoding.  The message ID is a valid Xapian
+query, quoted using Xapian boolean term quoting rules: if the ID contains
+whitespace or a close paren or starts with a double quote, it must be
+enclosed in double quotes and double quotes inside the ID must be doubled.
+The astute reader will notice this is a special case of the batch input
+format for \fBnotmuch-tag\fR(1); note that the single message-id query is
+mandatory for \fBnotmuch-restore\fR(1).
+
+.RE
+
+
 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.
index cd83a8835dd20703e906ed2e4e6e1ba87946acbc..2ee6a8f01cfdbc9e0e64efba08a6b80553efed77 100644 (file)
@@ -1,6 +1,6 @@
-.TH NOTMUCH-NEW 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-NEW 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-new \- Incorporate new mail into the notmuch database.
+notmuch-new \- incorporate new mail into the notmuch database
 .SH SYNOPSIS
 
 .B notmuch new
index fb5114cabf0b499065e3f4c3a1f4af2c6ed43088..2751e96be5871f8a57596a7448951a8e6740bd7a 100644 (file)
@@ -1,6 +1,6 @@
-.TH NOTMUCH-REPLY 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-REPLY 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-reply \- Constructs a reply template for a set of messages.
+notmuch-reply \- constructs a reply template for a set of messages
 
 .SH SYNOPSIS
 
@@ -37,7 +37,7 @@ Supported options for
 include
 .RS
 .TP 4
-.BR \-\-format= ( default | json | headers\-only )
+.BR \-\-format= ( default | json | sexp | headers\-only )
 .RS
 .TP 4
 .BR default
@@ -48,10 +48,25 @@ Produces JSON output containing headers for a reply message and the
 contents of the original message. This output can be used by a client
 to create a reply message intelligently.
 .TP
+.BR sexp
+Produces S-Expression output containing headers for a reply message and
+the contents of the original message. This output can be used by a client
+to create a reply message intelligently.
+.TP
 .BR headers\-only
 Only produces In\-Reply\-To, References, To, Cc, and Bcc headers.
 .RE
 .RE
+
+.RS
+.TP 4
+.BR \-\-format-version=N
+
+Use the specified structured output format version.  This is intended
+for programs that invoke \fBnotmuch\fR(1) internally.  If omitted, the
+latest supported version will be used.
+.RE
+
 .RS
 .TP 4
 .BR \-\-reply\-to= ( all | sender )
@@ -74,8 +89,8 @@ user's addresses.
 
 Decrypt any MIME encrypted parts found in the selected content
 (ie. "multipart/encrypted" parts). Status of the decryption will be
-reported (currently only supported with --format=json) and the
-multipart/encrypted part will be replaced by the decrypted
+reported (currently only supported with --format=json and --format=sexp)
+and the multipart/encrypted part will be replaced by the decrypted
 content.
 .RE
 
@@ -89,10 +104,22 @@ id:<message-id>), but it can be useful to reply to several messages at
 once. For example, when a series of patches are sent in a single
 thread, replying to the entire thread allows for the reply to comment
 on issues found in multiple patches. The default format supports
-replying to multiple messages at once, but the JSON format does not.
+replying to multiple messages at once, but the JSON and S-Expression
+formats do not.
 .RE
 .RE
 
+.SH EXIT STATUS
+
+This command supports the following special exit status codes
+
+.TP
+.B 20
+The requested format version is too old.
+.TP
+.B 21
+The requested format version is too new.
+
 .SH SEE ALSO
 
 \fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
index 3156af762cc0eb7ebcb41feaefed2414e4ccc403..52aae41eca7039fade6468939778ebc1f5eabdca 100644 (file)
@@ -1,12 +1,13 @@
-.TH NOTMUCH-RESTORE 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-RESTORE 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-restore \- Restores the tags from the given file (see notmuch dump).
+notmuch-restore \- restores the tags from the given file (see notmuch dump)
 
 .SH SYNOPSIS
 
 .B "notmuch restore"
 .RB [ "--accumulate" ]
-.RI "[ <" filename "> ]"
+.RB [ "--format=(auto|batch-tag|sup)" ]
+.RI "[ --input=<" filename "> ]"
 
 .SH DESCRIPTION
 
@@ -15,19 +16,49 @@ Restores the tags from the given file (see
 
 The input is read from the given filename, if any, or from stdin.
 
-Note: The dump file format is specifically chosen to be
+
+Supported options for
+.B restore
+include
+.RS 4
+.TP 4
+.B \-\-accumulate
+
+The union of the existing and new tags is applied, instead of
+replacing each message's tags as they are read in from the dump file.
+
+.RE
+.RS 4
+.TP 4
+.B \-\-format=(sup|batch-tag|auto)
+
+Notmuch restore supports two plain text dump formats, with each line
+specifying a message-id and a set of tags.
+For details of the actual formats, see \fBnotmuch-dump\fR(1).
+
+.RS 4
+.TP 4
+.B sup
+
+The
+.B sup
+dump file format is specifically chosen to be
 compatible with the format of files produced by sup-dump.
 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
+.RS 4
+.TP 4
+.B batch-tag
 
-See \fBnotmuch-search-terms\fR(7)
-for details of the supported syntax for <search-terms>.
+The
+.B batch-tag
+dump format is intended to more robust against malformed message-ids
+and tags containing whitespace or non-\fBascii\fR(7) characters.  See
+\fBnotmuch-dump\fR(1) for details on this format.
 
 .B "notmuch restore"
 updates the maildir flags according to tag changes if the
@@ -36,6 +67,20 @@ configuration option is enabled. See \fBnotmuch-config\fR(1) for
 details.
 
 .RE
+
+.RS 4
+.TP 4
+.B auto
+
+This option (the default) tries to guess the format from the
+input. For correctly formed input in either supported format, this
+heuristic, based the fact that batch-tag format contains no parentheses,
+should be accurate.
+
+.RE
+
+.RE
+
 .SH SEE ALSO
 
 \fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
index 5c72c4ba2ea4149a4ccc48b1c0ecfe7997b781a3..acd8863a6bc4c11d00236db48e39c4f32a0446f9 100644 (file)
@@ -1,6 +1,6 @@
-.TH NOTMUCH-SEARCH 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-SEARCH 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-search \- Search for messages matching the given search terms.
+notmuch-search \- search for messages matching the given search terms
 .SH SYNOPSIS
 
 .B notmuch search
@@ -25,9 +25,20 @@ Supported options for
 include
 .RS 4
 .TP 4
-.BR \-\-format= ( json | text )
+.BR \-\-format= ( json | sexp | text | text0 )
 
-Presents the results in either JSON or plain-text (default).
+Presents the results in either JSON, S-Expressions, newline character
+separated plain-text (default), or null character separated plain-text
+(compatible with \fBxargs\fR(1) -0 option where available).
+.RE
+
+.RS 4
+.TP 4
+.BR \-\-format-version=N
+
+Use the specified structured output format version.  This is intended
+for programs that invoke \fBnotmuch\fR(1) internally.  If omitted, the
+latest supported version will be used.
 .RE
 
 .RS 4
@@ -48,31 +59,36 @@ the authors of the thread and the subject.
 .B threads
 
 Output the thread IDs of all threads with any message matching the
-search terms, either one per line (\-\-format=text) or as a JSON array
-(\-\-format=json).
+search terms, either one per line (\-\-format=text), separated by null
+characters (\-\-format=text0), as a JSON array (\-\-format=json), or
+an S-Expression list (\-\-format=sexp).
 .RE
 .RS 4
 .TP 4
 .B messages
 
 Output the message IDs of all messages matching the search terms,
-either one per line (\-\-format=text) or as a JSON array
-(\-\-format=json).
+either one per line (\-\-format=text), separated by null characters
+(\-\-format=text0), as a JSON array (\-\-format=json), or as an
+S-Expression list (\-\-format=sexp).
 .RE
 .RS 4
 .TP 4
 .B files
 
 Output the filenames of all messages matching the search terms, either
-one per line (\-\-format=text) or as a JSON array (\-\-format=json).
+one per line (\-\-format=text), separated by null characters
+(\-\-format=text0), as a JSON array (\-\-format=json), or as an
+S-Expression list (\-\-format=sexp).
 .RE
 .RS 4
 .TP 4
 .B tags
 
 Output all tags that appear on any message matching the search terms,
-either one per line (\-\-format=text) or as a JSON array
-(\-\-format=json).
+either one per line (\-\-format=text), separated by null characters
+(\-\-format=text0), as a JSON array (\-\-format=json), or as an
+S-Expression list (\-\-format=sexp).
 .RE
 .RE
 
@@ -125,6 +141,17 @@ In this case all matching threads are returned but the "match count"
 is the number of matching non-excluded messages in the thread.
 .RE
 
+.SH EXIT STATUS
+
+This command supports the following special exit status codes
+
+.TP
+.B 20
+The requested format version is too old.
+.TP
+.B 21
+The requested format version is too new.
+
 .SH SEE ALSO
 
 \fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
index efd30a0213db5ec9c60db7c51eb2d4d1db5bd730..5d4ccfab3ec7174f6689d73cf31d09320bcba91b 100644 (file)
@@ -1,6 +1,6 @@
-.TH NOTMUCH-SHOW 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-SHOW 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-show \- Show messages matching the given search terms.
+notmuch-show \- show messages matching the given search terms
 .SH SYNOPSIS
 
 .B notmuch show
@@ -24,16 +24,21 @@ Supported options for
 include
 .RS 4
 .TP 4
-.B \-\-entire\-thread
+.B \-\-entire\-thread=(true|false)
 
-By default only those messages that match the search terms will be
-displayed. With this option, all messages in the same thread as any
-matched message will be displayed.
+If true,
+.B notmuch show
+outputs all messages in the thread of any message matching the search
+terms; if false, it outputs only the matching messages. For
+.B --format=json
+and
+.B --format=sexp
+this defaults to true.  For other formats, this defaults to false.
 .RE
 
 .RS 4
 .TP 4
-.B \-\-format=(text|json|mbox|raw)
+.B \-\-format=(text|json|sexp|mbox|raw)
 
 .RS 4
 .TP 4
@@ -55,11 +60,31 @@ be nested.
 The output is formatted with Javascript Object Notation (JSON). This
 format is more robust than the text format for automated
 processing. The nested structure of multipart MIME messages is
-reflected in nested JSON output. JSON output always includes all
-messages in a matching thread; in effect
+reflected in nested JSON output. By default JSON output includes all
+messages in a matching thread; that is, by default,
+
 .B \-\-format=json
-implies
-.B \-\-entire\-thread
+sets
+.B "\-\-entire\-thread"
+The caller can disable this behaviour by setting
+.B \-\-entire\-thread=false
+.RE
+.RS 4
+.TP 4
+.B sexp
+
+The output is formatted as an S-Expression (sexp). This
+format is more robust than the text format for automated
+processing. The nested structure of multipart MIME messages is
+reflected in nested S-Expression output. By default,
+S-Expression output includes all messages in a matching thread;
+that is, by default,
+
+.B \-\-format=sexp
+sets
+.B "\-\-entire\-thread"
+The caller can disable this behaviour by setting
+.B \-\-entire\-thread=false
 
 .RE
 .RS 4
@@ -101,6 +126,15 @@ message.
 .RE
 .RE
 
+.RS 4
+.TP 4
+.BR \-\-format-version=N
+
+Use the specified structured output format version.  This is intended
+for programs that invoke \fBnotmuch\fR(1) internally.  If omitted, the
+latest supported version will be used.
+.RE
+
 .RS 4
 .TP 4
 .B \-\-part=N
@@ -108,7 +142,7 @@ message.
 Output the single decoded MIME part N of a single message.  The search
 terms must match only a single message.  Message parts are numbered in
 a depth-first walk of the message MIME structure, and are identified
-in the 'json' or 'text' output formats.
+in the 'json', 'sexp' or 'text' output formats.
 .RE
 
 .RS 4
@@ -118,8 +152,8 @@ in the 'json' or 'text' output formats.
 Compute and report the validity of any MIME cryptographic signatures
 found in the selected content (ie. "multipart/signed" parts). Status
 of the signature will be reported (currently only supported with
---format=json), and the multipart/signed part will be replaced by the
-signed data.
+--format=json and --format=sexp), and the multipart/signed part
+will be replaced by the signed data.
 .RE
 
 .RS 4
@@ -128,9 +162,9 @@ signed data.
 
 Decrypt any MIME encrypted parts found in the selected content
 (ie. "multipart/encrypted" parts). Status of the decryption will be
-reported (currently only supported with --format=json) and the
-multipart/encrypted part will be replaced by the decrypted
-content.
+reported (currently only supported with --format=json and
+--format=sexp) and the multipart/encrypted part will be replaced
+by the decrypted content.  Implies --verify.
 .RE
 
 .RS 4
@@ -152,6 +186,22 @@ The default is
 
 .RE
 
+.RS 4
+.TP 4
+.B \-\-body=(true|false)
+
+If true (the default)
+.B notmuch show
+includes the bodies of the messages in the output; if false,
+bodies are omitted.
+.B --body=false
+is only implemented for the json and sexp formats and it is incompatible with
+.B --part > 0.
+
+This is useful if the caller only needs the headers as body-less
+output is much faster and substantially smaller.
+.RE
+
 A common use of
 .B notmuch show
 is to display a single thread of email messages. For this, use a
@@ -160,6 +210,17 @@ column of output from the
 .B notmuch search
 command.
 
+.SH EXIT STATUS
+
+This command supports the following special exit status codes
+
+.TP
+.B 20
+The requested format version is too old.
+.TP
+.B 21
+The requested format version is too new.
+
 .SH SEE ALSO
 
 \fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
index 27e682ef22dcf63d41e2f9fd17f7c867a94ec820..a65eb426005c29c0d9a6b21ca3086d3bf3d790e8 100644 (file)
@@ -1,23 +1,29 @@
-.TH NOTMUCH-TAG 1 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-TAG 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
-notmuch-tag \- Add/remove tags for all messages matching the search terms.
+notmuch-tag \- add/remove tags for all messages matching the search terms
 
 .SH SYNOPSIS
 .B notmuch tag
-.RI  "+<" tag> "|\-<" tag "> [...] [\-\-] <" search-term ">..."
+.RI "+<" tag ">|\-<" tag "> [...] [\-\-] <" search-term "> [...]"
+
+.B notmuch tag
+.RI "--batch"
+.RI "[ --input=<" filename "> ]"
+
 
 .SH DESCRIPTION
 
 Add/remove tags for all messages matching the search terms.
 
 See \fBnotmuch-search-terms\fR(7)
-for details of the supported syntax for <search-terms>.
+for details of the supported syntax for
+.RI < search-term >.
 
 Tags prefixed by '+' are added while those prefixed by '\-' are
-removed. For each message, tag removal is performed before tag
-addition.
+removed. For each message, tag changes are applied in the order they
+appear on the command line.
 
-The beginning of <search-terms> is recognized by the first
+The beginning of the search terms is recognized by the first
 argument that begins with neither '+' nor '\-'. Support for
 an initial search term beginning with '+' or '\-' is provided
 by allowing the user to specify a "\-\-" argument to separate
@@ -29,6 +35,93 @@ updates the maildir flags according to tag changes if the
 configuration option is enabled. See \fBnotmuch-config\fR(1) for
 details.
 
+Supported options for
+.B tag
+include
+.RS 4
+.TP 4
+.BR \-\-batch
+
+Read batch tagging operations from a file (stdin by default). This is more
+efficient than repeated
+.B notmuch tag
+invocations. See
+.B TAG FILE FORMAT
+below for the input format. This option is not compatible with
+specifying tagging on the command line.
+.RE
+
+.RS 4
+.TP 4
+.BR "\-\-input=" <filename>
+
+Read input from given file, instead of from stdin. Implies
+.BR --batch .
+
+.SH TAG FILE FORMAT
+
+The input must consist of lines of the format:
+
+.RI "+<" tag ">|\-<" tag "> [...] [\-\-] <" query ">"
+
+Each line is interpreted similarly to
+.B notmuch tag
+command line arguments. The delimiter is one or more spaces ' '. Any
+characters in
+.RI < tag >
+.B may
+be hex-encoded with %NN where NN is the hexadecimal value of the
+character. To hex-encode a character with a multi-byte UTF-8 encoding,
+hex-encode each byte.
+Any spaces in <tag>
+.B must
+be hex-encoded as %20. Any characters that are not
+part of
+.RI  < tag >
+.B must not
+be hex-encoded.
+
+In the future tag:"tag with spaces" style quoting may be supported for
+.RI < tag >
+as well;
+for this reason all double quote characters in
+.RI < tag >
+.B should
+be hex-encoded.
+
+The
+.RI < query >
+should be quoted using Xapian boolean term quoting rules: if a term
+contains whitespace or a close paren or starts with a double quote, it
+must be enclosed in double quotes (not including any prefix) and
+double quotes inside the term must be doubled (see below for
+examples).
+
+Leading and trailing space ' ' is ignored. Empty lines and lines
+beginning with '#' are ignored.
+
+.SS EXAMPLE
+
+The following shows a valid input to batch tagging. Note that only the
+isolated '*' acts as a wildcard. Also note the two different quotings
+of the tag
+.B space in tags
+.
+.RS
+.nf
++winner *
++foo::bar%25 -- (One and Two) or (One and tag:winner)
++found::it -- tag:foo::bar%
+# ignore this line and the next
+
++space%20in%20tags -- Two
+# add tag '(tags)', among other stunts.
++crazy{ +(tags) +&are +#possible\ -- tag:"space in tags"
++match*crazy -- tag:crazy{
++some_tag -- id:"this is ""nauty)"""
+.fi
+.RE
+
 .SH SEE ALSO
 
 \fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
index ebea4aa409c165e30c477ce702e1d4f37cb57127..9b25c275c3f880235beaf23c6424153e70478a3e 100644 (file)
@@ -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 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH 1 2013-01-24 "Notmuch 0.15.1"
 .SH NAME
 notmuch \- thread-based email index, search, and tagging
 .SH SYNOPSIS
index b914a29576c460ac278f02cf655890e5844b56da..a543d5d702513b849af9fd5d34ddae729e0506e7 100644 (file)
@@ -1,4 +1,4 @@
-.TH NOTMUCH-HOOKS 5 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-HOOKS 5 2013-01-24 "Notmuch 0.15.1"
 
 .SH NAME
 notmuch-hooks \- hooks for notmuch
index c559ed68b6573a8c2f24cd86bff80d8689646198..8916fdad6a3fb6503ff920e90fa5f82ca1148fba 100644 (file)
@@ -1,7 +1,7 @@
-.TH NOTMUCH-SEARCH-TERMS 7 2012-06-01 "Notmuch 0.13.2"
+.TH NOTMUCH-SEARCH-TERMS 7 2013-01-24 "Notmuch 0.15.1"
 
 .SH NAME
-notmuch-search-terms \- Syntax for notmuch queries
+notmuch-search-terms \- syntax for notmuch queries
 
 .SH SYNOPSIS
 
@@ -54,6 +54,8 @@ terms to match against specific portions of an email, (where
 
        folder:<directory-path>
 
+       date:<since>..<until>
+
 The
 .B from:
 prefix is used to match the name or address of the sender of an email
@@ -104,6 +106,26 @@ contained within particular directories within the mail store. Only
 the directory components below the top-level mail database path are
 available to be searched.
 
+The
+.B date:
+prefix can be used to restrict the results to only messages within a
+particular time range (based on the Date: header) with a range syntax
+of:
+
+       date:<since>..<until>
+
+See \fBDATE AND TIME SEARCH\fR below for details on the range
+expression, and supported syntax for <since> and <until> date and time
+expressions.
+
+The time range can also be specified using timestamps with a syntax
+of:
+
+       <initial-timestamp>..<final-timestamp>
+
+Each timestamp is a number representing the number of seconds since
+1970\-01\-01 00:00:00 UTC.
+
 In addition to individual terms, multiple terms can be
 combined with Boolean operators (
 .BR and ", " or ", " not
@@ -117,20 +139,124 @@ operators, but will have to be protected from interpretation by the
 shell, (such as by putting quotation marks around any parenthesized
 expression).
 
-Finally, results can be restricted to only messages within a
-particular time range, (based on the Date: header) with a syntax of:
+.SH DATE AND TIME SEARCH
 
-       <initial-timestamp>..<final-timestamp>
+notmuch understands a variety of standard and natural ways of
+expressing dates and times, both in absolute terms ("2012-10-24") and
+in relative terms ("yesterday"). Any number of relative terms can be
+combined ("1 hour 25 minutes") and an absolute date/time can be
+combined with relative terms to further adjust it. A non-exhaustive
+description of the syntax supported for absolute and relative terms is
+given below.
 
-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 following syntax would
-specify a date range to return messages from 2009\-10\-01 until the
-current time:
-
-       $(date +%s \-d 2009\-10\-01)..$(date +%s)
+.RS 4
+.TP 4
+.B The range expression
+
+date:<since>..<until>
+
+The above expression restricts the results to only messages from
+<since> to <until>, based on the Date: header.
+
+<since> and <until> can describe imprecise times, such as "yesterday".
+In this case, <since> is taken as the earliest time it could describe
+(the beginning of yesterday) and <until> is taken as the latest time
+it could describe (the end of yesterday). Similarly,
+date:january..february matches from the beginning of January to the
+end of February.
+
+Currently, we do not support spaces in range expressions. You can
+replace the spaces with '_', or (in most cases) '-', or (in some
+cases) leave the spaces out altogether. Examples in this man page use
+spaces for clarity.
+
+Open-ended ranges are supported (since Xapian 1.2.1), i.e. it's
+possible to specify date:..<until> or date:<since>.. to not limit the
+start or end time, respectively. Pre-1.2.1 Xapian does not report an
+error on open ended ranges, but it does not work as expected either.
+
+Entering date:expr without ".." (for example date:yesterday) won't
+work, as it's not interpreted as a range expression at all. You can
+achieve the expected result by duplicating the expr both sides of ".."
+(for example date:yesterday..yesterday).
+.RE
+
+.RS 4
+.TP 4
+.B Relative date and time
+[N|number] (years|months|weeks|days|hours|hrs|minutes|mins|seconds|secs) [...]
+
+All refer to past, can be repeated and will be accumulated.
+
+Units can be abbreviated to any length, with the otherwise ambiguous
+single m being m for minutes and M for months.
+
+Number can also be written out one, two, ..., ten, dozen,
+hundred. Additionally, the unit may be preceded by "last" or "this"
+(e.g., "last week" or "this month").
+
+When combined with absolute date and time, the relative date and time
+specification will be relative from the specified absolute date and
+time.
+
+Examples: 5M2d, two weeks
+.RE
+
+.RS 4
+.TP 4
+.B Supported absolute time formats
+H[H]:MM[:SS] [(am|a.m.|pm|p.m.)]
+
+H[H] (am|a.m.|pm|p.m.)
+
+HHMMSS
+
+now
+
+noon
+
+midnight
+
+Examples: 17:05, 5pm
+.RE
+
+.RS 4
+.TP 4
+.B Supported absolute date formats
+YYYY-MM[-DD]
+
+DD-MM[-[YY]YY]
+
+MM-YYYY
+
+M[M]/D[D][/[YY]YY]
+
+M[M]/YYYY
+
+D[D].M[M][.[YY]YY]
+
+D[D][(st|nd|rd|th)] Mon[thname] [YYYY]
+
+Mon[thname] D[D][(st|nd|rd|th)] [YYYY]
+
+Wee[kday]
+
+Month names can be abbreviated at three or more characters.
+
+Weekday names can be abbreviated at three or more characters.
+
+Examples: 2012-07-31, 31-07-2012, 7/31/2012, August 3
+.RE
+
+.RS 4
+.TP 4
+.B Time zones
+(+|-)HH:MM
+
+(+|-)HH[MM]
+
+Some time zone codes, e.g. UTC, EET.
+.RE
 
 .SH SEE ALSO
 
index a95bdabc43d86bffd8ce55eaffda2f2395825900..839737a8b354df36cec7f0bcea9ca05188618fcb 100644 (file)
@@ -33,12 +33,7 @@ typedef struct mime_node_context {
     GMimeMessage *mime_message;
 
     /* Context provided by the caller. */
-#ifdef GMIME_ATLEAST_26
-    GMimeCryptoContext *cryptoctx;
-#else
-    GMimeCipherContext *cryptoctx;
-#endif
-    notmuch_bool_t decrypt;
+    notmuch_crypto_t *crypto;
 } mime_node_context_t;
 
 static int
@@ -61,12 +56,7 @@ _mime_node_context_free (mime_node_context_t *res)
 
 notmuch_status_t
 mime_node_open (const void *ctx, notmuch_message_t *message,
-#ifdef GMIME_ATLEAST_26
-               GMimeCryptoContext *cryptoctx,
-#else
-               GMimeCipherContext *cryptoctx,
-#endif
-               notmuch_bool_t decrypt, mime_node_t **root_out)
+               notmuch_crypto_t *crypto, mime_node_t **root_out)
 {
     const char *filename = notmuch_message_get_filename (message);
     mime_node_context_t *mctx;
@@ -118,8 +108,7 @@ mime_node_open (const void *ctx, notmuch_message_t *message,
        goto DONE;
     }
 
-    mctx->cryptoctx = cryptoctx;
-    mctx->decrypt = decrypt;
+    mctx->crypto = crypto;
 
     /* Create the root node */
     root->part = GMIME_OBJECT (mctx->mime_message);
@@ -161,6 +150,7 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part)
 {
     mime_node_t *node = talloc_zero (parent, mime_node_t);
     GError *err = NULL;
+    notmuch_crypto_context_t *cryptoctx = NULL;
 
     /* Set basic node properties */
     node->part = part;
@@ -193,9 +183,15 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part)
        return NULL;
     }
 
+    if ((GMIME_IS_MULTIPART_ENCRYPTED (part) && node->ctx->crypto->decrypt)
+       || (GMIME_IS_MULTIPART_SIGNED (part) && node->ctx->crypto->verify)) {
+       GMimeContentType *content_type = g_mime_object_get_content_type (part);
+       const char *protocol = g_mime_content_type_get_parameter (content_type, "protocol");
+       cryptoctx = notmuch_crypto_get_context (node->ctx->crypto, protocol);
+    }
+
     /* Handle PGP/MIME parts */
-    if (GMIME_IS_MULTIPART_ENCRYPTED (part)
-       && node->ctx->cryptoctx && node->ctx->decrypt) {
+    if (GMIME_IS_MULTIPART_ENCRYPTED (part) && node->ctx->crypto->decrypt && cryptoctx) {
        if (node->nchildren != 2) {
            /* this violates RFC 3156 section 4, so we won't bother with it. */
            fprintf (stderr, "Error: %d part(s) for a multipart/encrypted "
@@ -208,10 +204,10 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part)
 #ifdef GMIME_ATLEAST_26
            GMimeDecryptResult *decrypt_result = NULL;
            node->decrypted_child = g_mime_multipart_encrypted_decrypt
-               (encrypteddata, node->ctx->cryptoctx, &decrypt_result, &err);
+               (encrypteddata, cryptoctx, &decrypt_result, &err);
 #else
            node->decrypted_child = g_mime_multipart_encrypted_decrypt
-               (encrypteddata, node->ctx->cryptoctx, &err);
+               (encrypteddata, cryptoctx, &err);
 #endif
            if (node->decrypted_child) {
                node->decrypt_success = node->verify_attempted = TRUE;
@@ -229,7 +225,7 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part)
                         (err ? err->message : "no error explanation given"));
            }
        }
-    } else if (GMIME_IS_MULTIPART_SIGNED (part) && node->ctx->cryptoctx) {
+    } else if (GMIME_IS_MULTIPART_SIGNED (part) && node->ctx->crypto->verify && cryptoctx) {
        if (node->nchildren != 2) {
            /* this violates RFC 3156 section 5, so we won't bother with it. */
            fprintf (stderr, "Error: %d part(s) for a multipart/signed message "
@@ -238,7 +234,7 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part)
        } else {
 #ifdef GMIME_ATLEAST_26
            node->sig_list = g_mime_multipart_signed_verify
-               (GMIME_MULTIPART_SIGNED (part), node->ctx->cryptoctx, &err);
+               (GMIME_MULTIPART_SIGNED (part), cryptoctx, &err);
            node->verify_attempted = TRUE;
 
            if (!node->sig_list)
@@ -254,7 +250,7 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part)
             * In GMime 2.6, they're both non-const, so we'll be able
             * to clean up this asymmetry. */
            GMimeSignatureValidity *sig_validity = g_mime_multipart_signed_verify
-               (GMIME_MULTIPART_SIGNED (part), node->ctx->cryptoctx, &err);
+               (GMIME_MULTIPART_SIGNED (part), cryptoctx, &err);
            node->verify_attempted = TRUE;
            node->sig_validity = sig_validity;
            if (sig_validity) {
@@ -295,7 +291,7 @@ mime_node_child (mime_node_t *parent, int child)
     GMimeObject *sub;
     mime_node_t *node;
 
-    if (!parent || child < 0 || child >= parent->nchildren)
+    if (!parent || !parent->part || child < 0 || child >= parent->nchildren)
        return NULL;
 
     if (GMIME_IS_MULTIPART (parent->part)) {
index 19b7f01f09889fe06be8864fa179a7a931e5af78..5f2883681c1ee127332108ba7872146a04eb3e6f 100644 (file)
@@ -36,6 +36,9 @@
  * these to check the version number. */
 #ifdef GMIME_MAJOR_VERSION
 #define GMIME_ATLEAST_26
+typedef GMimeCryptoContext notmuch_crypto_context_t;
+#else
+typedef GMimeCipherContext notmuch_crypto_context_t;
 #endif
 
 #include "notmuch.h"
@@ -55,7 +58,7 @@
 #include <errno.h>
 #include <signal.h>
 
-#include <talloc.h>
+#include "talloc-extra.h"
 
 #define unused(x) x __attribute__ ((unused))
 
 #define STRINGIFY_(s) #s
 
 typedef struct mime_node mime_node_t;
+struct sprinter;
 struct notmuch_show_params;
 
 typedef struct notmuch_show_format {
-    const char *message_set_start;
-    notmuch_status_t (*part) (const void *ctx,
+    struct sprinter *(*new_sprinter) (const void *ctx, FILE *stream);
+    notmuch_status_t (*part) (const void *ctx, struct sprinter *sprinter,
                              struct mime_node *node, int indent,
                              const struct notmuch_show_params *params);
-    const char *message_set_sep;
-    const char *message_set_end;
 } notmuch_show_format_t;
 
+typedef struct notmuch_crypto {
+    notmuch_crypto_context_t* gpgctx;
+    notmuch_bool_t verify;
+    notmuch_bool_t decrypt;
+} notmuch_crypto_t;
+
 typedef struct notmuch_show_params {
     notmuch_bool_t entire_thread;
     notmuch_bool_t omit_excluded;
+    notmuch_bool_t output_body;
     notmuch_bool_t raw;
     int part;
-#ifdef GMIME_ATLEAST_26
-    GMimeCryptoContext* cryptoctx;
-#else
-    GMimeCipherContext* cryptoctx;
-#endif
-    notmuch_bool_t decrypt;
+    notmuch_crypto_t crypto;
 } notmuch_show_params_t;
 
 /* There's no point in continuing when we've detected that we've done
@@ -113,6 +117,57 @@ chomp_newline (char *str)
        str[strlen(str)-1] = '\0';
 }
 
+/* Exit status code indicating the requested format version is too old
+ * (support for that version has been dropped).  CLI code should use
+ * notmuch_exit_if_unsupported_format rather than directly exiting
+ * with this code.
+ */
+#define NOTMUCH_EXIT_FORMAT_TOO_OLD 20
+/* Exit status code indicating the requested format version is newer
+ * than the version supported by the CLI.  CLI code should use
+ * notmuch_exit_if_unsupported_format rather than directly exiting
+ * with this code.
+ */
+#define NOTMUCH_EXIT_FORMAT_TOO_NEW 21
+
+/* The current structured output format version.  Requests for format
+ * versions above this will return an error.  Backwards-incompatible
+ * changes such as removing map fields, changing the meaning of map
+ * fields, or changing the meanings of list elements should increase
+ * this.  New (required) map fields can be added without increasing
+ * this.
+ */
+#define NOTMUCH_FORMAT_CUR 1
+/* The minimum supported structured output format version.  Requests
+ * for format versions below this will return an error. */
+#define NOTMUCH_FORMAT_MIN 1
+
+/* The output format version requested by the caller on the command
+ * line.  If no format version is requested, this will be set to
+ * NOTMUCH_FORMAT_CUR.  Even though the command-line option is
+ * per-command, this is global because commands can share structured
+ * output code.
+ */
+extern int notmuch_format_version;
+
+/* Commands that support structured output should support the
+ * following argument
+ *  { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 }
+ * and should invoke notmuch_exit_if_unsupported_format to check the
+ * requested version.  If notmuch_format_version is outside the
+ * supported range, this will print a detailed diagnostic message for
+ * the user and exit with NOTMUCH_EXIT_FORMAT_TOO_{OLD,NEW} to inform
+ * the invoking program of the problem.
+ */
+void
+notmuch_exit_if_unsupported_format (void);
+
+notmuch_crypto_context_t *
+notmuch_crypto_get_context (notmuch_crypto_t *crypto, const char *protocol);
+
+int
+notmuch_crypto_cleanup (notmuch_crypto_t *crypto);
+
 int
 notmuch_count_command (void *ctx, int argc, char *argv[]);
 
@@ -165,10 +220,12 @@ notmuch_status_t
 show_one_part (const char *filename, int part);
 
 void
-format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first);
+format_part_sprinter (const void *ctx, struct sprinter *sp, mime_node_t *node,
+                     notmuch_bool_t first, notmuch_bool_t output_body);
 
 void
-format_headers_json (const void *ctx, GMimeMessage *message, notmuch_bool_t reply);
+format_headers_sprinter (struct sprinter *sp, GMimeMessage *message,
+                        notmuch_bool_t reply);
 
 typedef enum {
     NOTMUCH_SHOW_TEXT_PART_REPLY = 1 << 0,
@@ -341,9 +398,10 @@ struct mime_node {
 };
 
 /* Construct a new MIME node pointing to the root message part of
- * message.  If cryptoctx is non-NULL, it will be used to verify
- * signatures on any child parts.  If decrypt is true, then cryptoctx
- * will additionally be used to decrypt any encrypted child parts.
+ * message. If crypto->verify is true, signed child parts will be
+ * verified. If crypto->decrypt is true, encrypted child parts will be
+ * decrypted.  If crypto->gpgctx is NULL, it will be lazily
+ * initialized.
  *
  * Return value:
  *
@@ -355,12 +413,7 @@ struct mime_node {
  */
 notmuch_status_t
 mime_node_open (const void *ctx, notmuch_message_t *message,
-#ifdef GMIME_ATLEAST_26
-               GMimeCryptoContext *cryptoctx,
-#else
-               GMimeCipherContext *cryptoctx,
-#endif
-               notmuch_bool_t decrypt, mime_node_t **node_out);
+               notmuch_crypto_t *crypto, mime_node_t **node_out);
 
 /* Return a new MIME node for the requested child part of parent.
  * parent will be used as the talloc context for the returned child
index 3e37a2d654b5c57df4d3a39d07afe97b5e9095e6..b5c2066e345678fa36f189116c2d6b243eccf60e 100644 (file)
@@ -49,8 +49,9 @@ static const char new_config_comment[] =
     "\tignore  A list (separated by ';') of file and directory names\n"
     "\t        that will not be searched for messages by \"notmuch new\".\n"
     "\n"
-    "\t        NOTE: *Every* file/directory that goes by one of those names will\n"
-    "\t        be ignored, independent of its depth/location in the mail store.\n";
+    "\t        NOTE: *Every* file/directory that goes by one of those\n"
+    "\t        names will be ignored, independent of its depth/location\n"
+    "\t        in the mail store.\n";
 
 static const char user_config_comment[] =
     " User configuration\n"
index 37432142761216e4a87494c3ac57bf79ad207c99..a3244e0a417e093945113154bf5d213a9eb87633 100644 (file)
@@ -19,6 +19,8 @@
  */
 
 #include "notmuch-client.h"
+#include "dump-restore-private.h"
+#include "string-util.h"
 
 int
 notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
@@ -30,7 +32,7 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
     notmuch_messages_t *messages;
     notmuch_message_t *message;
     notmuch_tags_t *tags;
-    const charquery_str = "";
+    const char *query_str = "";
 
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
@@ -43,8 +45,14 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
     char *output_file_name = NULL;
     int opt_index;
 
+    int output_format = DUMP_FORMAT_SUP;
+
     notmuch_opt_desc_t options[] = {
-       { NOTMUCH_OPT_POSITION, &output_file_name, 0, 0, 0  },
+       { NOTMUCH_OPT_KEYWORD, &output_format, "format", 'f',
+         (notmuch_keyword_t []){ { "sup", DUMP_FORMAT_SUP },
+                                 { "batch-tag", DUMP_FORMAT_BATCH_TAG },
+                                 { 0, 0 } } },
+       { NOTMUCH_OPT_STRING, &output_file_name, "output", 'o', 0  },
        { 0, 0, 0, 0, 0 }
     };
 
@@ -56,7 +64,6 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
     }
 
     if (output_file_name) {
-       fprintf (stderr, "Warning: the output file argument of dump is deprecated.\n");
        output = fopen (output_file_name, "w");
        if (output == NULL) {
            fprintf (stderr, "Error opening %s for writing: %s\n",
@@ -67,7 +74,7 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
 
 
     if (opt_index < argc) {
-       query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
+       query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
        if (query_str == NULL) {
            fprintf (stderr, "Out of memory.\n");
            return 1;
@@ -84,29 +91,68 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
      */
     notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
 
+    char *buffer = NULL;
+    size_t buffer_size = 0;
+
     for (messages = notmuch_query_search_messages (query);
         notmuch_messages_valid (messages);
-        notmuch_messages_move_to_next (messages))
-    {
+        notmuch_messages_move_to_next (messages)) {
        int first = 1;
+       const char *message_id;
+
        message = notmuch_messages_get (messages);
+       message_id = notmuch_message_get_message_id (message);
+
+       if (output_format == DUMP_FORMAT_BATCH_TAG &&
+           strchr (message_id, '\n')) {
+           /* This will produce a line break in the output, which
+            * would be difficult to handle in tools.  However, it's
+            * also impossible to produce an email containing a line
+            * break in a message ID because of unfolding, so we can
+            * safely disallow it. */
+           fprintf (stderr, "Warning: skipping message id containing line break: \"%s\"\n", message_id);
+           notmuch_message_destroy (message);
+           continue;
+       }
 
-       fprintf (output,
-                "%s (", notmuch_message_get_message_id (message));
+       if (output_format == DUMP_FORMAT_SUP) {
+           fprintf (output, "%s (", message_id);
+       }
 
        for (tags = notmuch_message_get_tags (message);
             notmuch_tags_valid (tags);
-            notmuch_tags_move_to_next (tags))
-       {
-           if (! first)
-               fprintf (output, " ");
+            notmuch_tags_move_to_next (tags)) {
+           const char *tag_str = notmuch_tags_get (tags);
 
-           fprintf (output, "%s", notmuch_tags_get (tags));
+           if (! first)
+               fputs (" ", output);
 
            first = 0;
+
+           if (output_format == DUMP_FORMAT_SUP) {
+               fputs (tag_str, output);
+           } else {
+               if (hex_encode (notmuch, tag_str,
+                               &buffer, &buffer_size) != HEX_SUCCESS) {
+                   fprintf (stderr, "Error: failed to hex-encode tag %s\n",
+                            tag_str);
+                   return 1;
+               }
+               fprintf (output, "+%s", buffer);
+           }
        }
 
-       fprintf (output, ")\n");
+       if (output_format == DUMP_FORMAT_SUP) {
+           fputs (")\n", output);
+       } else {
+           if (make_boolean_term (notmuch, "id", message_id,
+                                  &buffer, &buffer_size)) {
+                   fprintf (stderr, "Error quoting message id %s: %s\n",
+                            message_id, strerror (errno));
+                   return 1;
+           }
+           fprintf (output, " -- %s\n", buffer);
+       }
 
        notmuch_message_destroy (message);
     }
index 72dd558d0fa150288eb15a14ea6aee5f6b00f135..feb9c32fda52e6b9d6d5490320e86f5a107b03ac 100644 (file)
@@ -36,7 +36,8 @@ typedef struct _filename_list {
 
 typedef struct {
     int output_is_a_tty;
-    int verbose;
+    notmuch_bool_t verbose;
+    notmuch_bool_t debug;
     const char **new_tags;
     size_t new_tags_length;
     const char **new_ignore;
@@ -154,6 +155,48 @@ dirent_sort_strcmp_name (const struct dirent **a, const struct dirent **b)
     return strcmp ((*a)->d_name, (*b)->d_name);
 }
 
+/* Return the type of a directory entry relative to path as a stat(2)
+ * mode.  Like stat, this follows symlinks.  Returns -1 and sets errno
+ * if the file's type cannot be determined (which includes dangling
+ * symlinks).
+ */
+static int
+dirent_type (const char *path, const struct dirent *entry)
+{
+    struct stat statbuf;
+    char *abspath;
+    int err, saved_errno;
+
+#ifdef _DIRENT_HAVE_D_TYPE
+    /* Mapping from d_type to stat mode_t.  We omit DT_LNK so that
+     * we'll fall through to stat and get the real file type. */
+    static const mode_t modes[] = {
+       [DT_BLK]  = S_IFBLK,
+       [DT_CHR]  = S_IFCHR,
+       [DT_DIR]  = S_IFDIR,
+       [DT_FIFO] = S_IFIFO,
+       [DT_REG]  = S_IFREG,
+       [DT_SOCK] = S_IFSOCK
+    };
+    if (entry->d_type < ARRAY_SIZE(modes) && modes[entry->d_type])
+       return modes[entry->d_type];
+#endif
+
+    abspath = talloc_asprintf (NULL, "%s/%s", path, entry->d_name);
+    if (!abspath) {
+       errno = ENOMEM;
+       return -1;
+    }
+    err = stat(abspath, &statbuf);
+    saved_errno = errno;
+    talloc_free (abspath);
+    if (err < 0) {
+       errno = saved_errno;
+       return -1;
+    }
+    return statbuf.st_mode & S_IFMT;
+}
+
 /* Test if the directory looks like a Maildir directory.
  *
  * Search through the array of directory entries to see if we can find all
@@ -162,12 +205,12 @@ dirent_sort_strcmp_name (const struct dirent **a, const struct dirent **b)
  * Return 1 if the directory looks like a Maildir and 0 otherwise.
  */
 static int
-_entries_resemble_maildir (struct dirent **entries, int count)
+_entries_resemble_maildir (const char *path, struct dirent **entries, int count)
 {
     int i, found = 0;
 
     for (i = 0; i < count; i++) {
-       if (entries[i]->d_type != DT_DIR && entries[i]->d_type != DT_UNKNOWN)
+       if (dirent_type (path, entries[i]) != S_IFDIR)
            continue;
 
        if (strcmp(entries[i]->d_name, "new") == 0 ||
@@ -239,9 +282,9 @@ _entry_in_ignore_list (const char *entry, add_files_state_t *state)
  *     if fs_mtime isn't the current wall-clock time.
  */
 static notmuch_status_t
-add_files_recursive (notmuch_database_t *notmuch,
-                    const char *path,
-                    add_files_state_t *state)
+add_files (notmuch_database_t *notmuch,
+          const char *path,
+          add_files_state_t *state)
 {
     DIR *dir = NULL;
     struct dirent *entry = NULL;
@@ -250,7 +293,7 @@ add_files_recursive (notmuch_database_t *notmuch,
     notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
     notmuch_message_t *message = NULL;
     struct dirent **fs_entries = NULL;
-    int i, num_fs_entries = 0;
+    int i, num_fs_entries = 0, entry_type;
     notmuch_directory_t *directory;
     notmuch_filenames_t *db_files = NULL;
     notmuch_filenames_t *db_subdirs = NULL;
@@ -266,11 +309,10 @@ add_files_recursive (notmuch_database_t *notmuch,
     }
     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
-     * that until this stat. */
-    if (! S_ISDIR (st.st_mode))
-       return NOTMUCH_STATUS_SUCCESS;
+    if (! S_ISDIR (st.st_mode)) {
+       fprintf (stderr, "Error: %s is not a directory.\n", path);
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
 
     fs_mtime = st.st_mtime;
 
@@ -300,7 +342,7 @@ add_files_recursive (notmuch_database_t *notmuch,
     }
 
     /* Pass 1: Recurse into all sub-directories. */
-    is_maildir = _entries_resemble_maildir (fs_entries, num_fs_entries);
+    is_maildir = _entries_resemble_maildir (path, fs_entries, num_fs_entries);
 
     for (i = 0; i < num_fs_entries; i++) {
        if (interrupted)
@@ -308,36 +350,43 @@ add_files_recursive (notmuch_database_t *notmuch,
 
        entry = fs_entries[i];
 
-       /* We only want to descend into directories.
-        * But symlinks can be to directories too, of course.
-        *
-        * And if the filesystem doesn't tell us the file type in the
-        * scandir results, then it might be a directory (and if not,
-        * then we'll stat and return immediately in the next level of
-        * recursion). */
-       if (entry->d_type != DT_DIR &&
-           entry->d_type != DT_LNK &&
-           entry->d_type != DT_UNKNOWN)
-       {
+       /* Ignore any files/directories the user has configured to
+        * ignore.  We do this before dirent_type both for performance
+        * and because we don't care if dirent_type fails on entries
+        * that are explicitly ignored.
+        */
+       if (_entry_in_ignore_list (entry->d_name, state)) {
+           if (state->debug)
+               printf ("(D) add_files_recursive, pass 1: explicitly ignoring %s/%s\n",
+                       path, entry->d_name);
+           continue;
+       }
+
+       /* We only want to descend into directories (and symlinks to
+        * directories). */
+       entry_type = dirent_type (path, entry);
+       if (entry_type == -1) {
+           /* Be pessimistic, e.g. so we don't lose lots of mail just
+            * because a user broke a symlink. */
+           fprintf (stderr, "Error reading file %s/%s: %s\n",
+                    path, entry->d_name, strerror (errno));
+           return NOTMUCH_STATUS_FILE_ERROR;
+       } else if (entry_type != S_IFDIR) {
            continue;
        }
 
        /* Ignore special directories to avoid infinite recursion.
-        * Also ignore the .notmuch directory, any "tmp" directory
-        * that appears within a maildir and files/directories
-        * the user has configured to be ignored.
+        * Also ignore the .notmuch directory and any "tmp" directory
+        * that appears within a maildir.
         */
        if (strcmp (entry->d_name, ".") == 0 ||
            strcmp (entry->d_name, "..") == 0 ||
            (is_maildir && strcmp (entry->d_name, "tmp") == 0) ||
-           strcmp (entry->d_name, ".notmuch") == 0 ||
-           _entry_in_ignore_list (entry->d_name, state))
-       {
+           strcmp (entry->d_name, ".notmuch") == 0)
            continue;
-       }
 
        next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
-       status = add_files_recursive (notmuch, next, state);
+       status = add_files (notmuch, next, state);
        if (status) {
            ret = status;
            goto DONE;
@@ -374,8 +423,13 @@ add_files_recursive (notmuch_database_t *notmuch,
         entry = fs_entries[i];
 
        /* Ignore files & directories user has configured to be ignored */
-       if (_entry_in_ignore_list (entry->d_name, state))
+       if (_entry_in_ignore_list (entry->d_name, state)) {
+           if (state->debug)
+               printf ("(D) add_files_recursive, pass 2: explicitly ignoring %s/%s\n",
+                       path,
+                       entry->d_name);
            continue;
+       }
 
        /* Check if we've walked past any names in db_files or
         * db_subdirs. If so, these have been deleted. */
@@ -407,31 +461,13 @@ add_files_recursive (notmuch_database_t *notmuch,
            notmuch_filenames_move_to_next (db_subdirs);
        }
 
-       /* 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 filesystem
-        * limitations), then we also need to look closer.
-        *
-        * In either case, a stat does the trick.
-        */
-       if (entry->d_type == DT_LNK || entry->d_type == DT_UNKNOWN) {
-           int err;
-
-           next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
-           err = stat (next, &st);
-           talloc_free (next);
-           next = NULL;
-
-           /* Don't emit an error for a link pointing nowhere, since
-            * the directory-traversal pass will have already done
-            * that. */
-           if (err)
-               continue;
-
-           if (! S_ISREG (st.st_mode))
-               continue;
-       } else if (entry->d_type != DT_REG) {
+       /* Only add regular files (and symlinks to regular files). */
+       entry_type = dirent_type (path, entry);
+       if (entry_type == -1) {
+           fprintf (stderr, "Error reading file %s/%s: %s\n",
+                    path, entry->d_name, strerror (errno));
+           return NOTMUCH_STATUS_FILE_ERROR;
+       } else if (entry_type != S_IFREG) {
            continue;
        }
 
@@ -625,32 +661,6 @@ stop_progress_printing_timer (void)
 }
 
 
-/* This is the top-level entry point for add_files. It does a couple
- * of error checks and then calls into the recursive function. */
-static notmuch_status_t
-add_files (notmuch_database_t *notmuch,
-          const char *path,
-          add_files_state_t *state)
-{
-    notmuch_status_t status;
-    struct stat st;
-
-    if (stat (path, &st)) {
-       fprintf (stderr, "Error reading directory %s: %s\n",
-                path, strerror (errno));
-       return NOTMUCH_STATUS_FILE_ERROR;
-    }
-
-    if (! S_ISDIR (st.st_mode)) {
-       fprintf (stderr, "Error: %s is not a directory.\n", path);
-       return NOTMUCH_STATUS_FILE_ERROR;
-    }
-
-    status = add_files_recursive (notmuch, path, state);
-
-    return status;
-}
-
 /* XXX: This should be merged with the add_files function since it
  * shares a lot of logic with it. */
 /* Recursively count all regular files in path and all sub-directories
@@ -688,6 +698,10 @@ count_files (const char *path, int *count, add_files_state_t *state)
            strcmp (entry->d_name, ".notmuch") == 0 ||
            _entry_in_ignore_list (entry->d_name, state))
        {
+           if (_entry_in_ignore_list (entry->d_name, state) && state->debug)
+               printf ("(D) count_files: explicitly ignoring %s/%s\n",
+                       path,
+                       entry->d_name);
            continue;
        }
 
@@ -839,25 +853,28 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
     char *dot_notmuch_path;
     struct sigaction action;
     _filename_node_t *f;
+    int opt_index;
     int i;
     notmuch_bool_t timer_is_active = FALSE;
-    notmuch_bool_t run_hooks = TRUE;
+    notmuch_bool_t no_hooks = FALSE;
 
-    add_files_state.verbose = 0;
+    add_files_state.verbose = FALSE;
+    add_files_state.debug = FALSE;
     add_files_state.output_is_a_tty = isatty (fileno (stdout));
 
-    argc--; argv++; /* skip subcommand argument */
+    notmuch_opt_desc_t options[] = {
+       { NOTMUCH_OPT_BOOLEAN,  &add_files_state.verbose, "verbose", 'v', 0 },
+       { NOTMUCH_OPT_BOOLEAN,  &add_files_state.debug, "debug", 'd', 0 },
+       { NOTMUCH_OPT_BOOLEAN,  &no_hooks, "no-hooks", 'n', 0 },
+       { 0, 0, 0, 0, 0 }
+    };
 
-    for (i = 0; i < argc && argv[i][0] == '-'; i++) {
-       if (STRNCMP_LITERAL (argv[i], "--verbose") == 0) {
-           add_files_state.verbose = 1;
-       } else if (strcmp (argv[i], "--no-hooks") == 0) {
-           run_hooks = FALSE;
-       } else {
-           fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
-           return 1;
-       }
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0) {
+       /* diagnostics already printed */
+       return 1;
     }
+
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
        return 1;
@@ -867,7 +884,7 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
     add_files_state.synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
     db_path = notmuch_config_get_database_path (config);
 
-    if (run_hooks) {
+    if (!no_hooks) {
        ret = notmuch_run_hook (db_path, "pre-new");
        if (ret)
            return ret;
@@ -1028,7 +1045,7 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
 
     notmuch_database_destroy (notmuch);
 
-    if (run_hooks && !ret && !interrupted)
+    if (!no_hooks && !ret && !interrupted)
        ret = notmuch_run_hook (db_path, "post-new");
 
     return ret || interrupted;
index 7184a5dfcf887bb6fcb81bc8f1ee63171f7bb286..22c58ff36776f87ade88dcf8ead0b5d2a97076a9 100644 (file)
@@ -22,6 +22,7 @@
 
 #include "notmuch-client.h"
 #include "gmime-filter-headers.h"
+#include "sprinter.h"
 
 static void
 show_reply_headers (GMimeMessage *message)
@@ -98,25 +99,77 @@ format_part_reply (mime_node_t *node)
        format_part_reply (mime_node_child (node, i));
 }
 
-/* Is the given address configured as one of the user's "personal" or
- * "other" addresses. */
-static int
-address_is_users (const char *address, notmuch_config_t *config)
+typedef enum {
+    USER_ADDRESS_IN_STRING,
+    STRING_IN_USER_ADDRESS,
+    STRING_IS_USER_ADDRESS,
+} address_match_t;
+
+/* Match given string against given address according to mode. */
+static notmuch_bool_t
+match_address (const char *str, const char *address, address_match_t mode)
+{
+    switch (mode) {
+    case USER_ADDRESS_IN_STRING:
+       return strcasestr (str, address) != NULL;
+    case STRING_IN_USER_ADDRESS:
+       return strcasestr (address, str) != NULL;
+    case STRING_IS_USER_ADDRESS:
+       return strcasecmp (address, str) == 0;
+    }
+
+    return FALSE;
+}
+
+/* Match given string against user's configured "primary" and "other"
+ * addresses according to mode. */
+static const char *
+address_match (const char *str, notmuch_config_t *config, address_match_t mode)
 {
     const char *primary;
     const char **other;
     size_t i, other_len;
 
+    if (!str || *str == '\0')
+       return NULL;
+
     primary = notmuch_config_get_user_primary_email (config);
-    if (strcasecmp (primary, address) == 0)
-       return 1;
+    if (match_address (str, primary, mode))
+       return primary;
 
     other = notmuch_config_get_user_other_email (config, &other_len);
-    for (i = 0; i < other_len; i++)
-       if (strcasecmp (other[i], address) == 0)
-           return 1;
+    for (i = 0; i < other_len; i++) {
+       if (match_address (str, other[i], mode))
+           return other[i];
+    }
 
-    return 0;
+    return NULL;
+}
+
+/* Does the given string contain an address configured as one of the
+ * user's "primary" or "other" addresses. If so, return the matching
+ * address, NULL otherwise. */
+static const char *
+user_address_in_string (const char *str, notmuch_config_t *config)
+{
+    return address_match (str, config, USER_ADDRESS_IN_STRING);
+}
+
+/* Do any of the addresses configured as one of the user's "primary"
+ * or "other" addresses contain the given string. If so, return the
+ * matching address, NULL otherwise. */
+static const char *
+string_in_user_address (const char *str, notmuch_config_t *config)
+{
+    return address_match (str, config, STRING_IN_USER_ADDRESS);
+}
+
+/* Is the given address configured as one of the user's "primary" or
+ * "other" addresses. */
+static notmuch_bool_t
+address_is_users (const char *address, notmuch_config_t *config)
+{
+    return address_match (address, config, STRING_IS_USER_ADDRESS) != NULL;
 }
 
 /* Scan addresses in 'list'.
@@ -325,19 +378,18 @@ add_recipients_from_message (GMimeMessage *reply,
 static const char *
 guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message)
 {
-    const char *received,*primary,*by;
-    const char **other;
-    char *tohdr;
+    const char *addr, *received, *by;
     char *mta,*ptr,*token;
     char *domain=NULL;
     char *tld=NULL;
     const char *delim=". \t";
-    size_t i,j,other_len;
-
-    const char *to_headers[] = {"Envelope-to", "X-Original-To"};
+    size_t i;
 
-    primary = notmuch_config_get_user_primary_email (config);
-    other = notmuch_config_get_user_other_email (config, &other_len);
+    const char *to_headers[] = {
+       "Envelope-to",
+       "X-Original-To",
+       "Delivered-To",
+    };
 
     /* sadly, there is no standard way to find out to which email
      * address a mail was delivered - what is in the headers depends
@@ -348,28 +400,19 @@ guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message
      * the To: or Cc: header. From here we try the following in order:
      * 1) check for an Envelope-to: header
      * 2) check for an X-Original-To: header
-     * 3) check for a (for <email@add.res>) clause in Received: headers
-     * 4) check for the domain part of known email addresses in the
+     * 3) check for a Delivered-To: header
+     * 4) check for a (for <email@add.res>) clause in Received: headers
+     * 5) check for the domain part of known email addresses in the
      *    'by' part of Received headers
      * If none of these work, we give up and return NULL
      */
-    for (i = 0; i < sizeof(to_headers)/sizeof(*to_headers); i++) {
-       tohdr = xstrdup(notmuch_message_get_header (message, to_headers[i]));
-       if (tohdr && *tohdr) {
-           /* tohdr is potentialy a list of email addresses, so here we
-            * check if one of the email addresses is a substring of tohdr
-            */
-           if (strcasestr(tohdr, primary)) {
-               free(tohdr);
-               return primary;
-           }
-           for (j = 0; j < other_len; j++)
-               if (strcasestr (tohdr, other[j])) {
-                   free(tohdr);
-                   return other[j];
-               }
-           free(tohdr);
-       }
+    for (i = 0; i < ARRAY_SIZE (to_headers); i++) {
+       const char *tohdr = notmuch_message_get_header (message, to_headers[i]);
+
+       /* Note: tohdr potentially contains a list of email addresses. */
+       addr = user_address_in_string (tohdr, config);
+       if (addr)
+           return addr;
     }
 
     /* We get the concatenated Received: headers and search from the
@@ -387,19 +430,12 @@ guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message
      * header
      */
     ptr = strstr (received, " for ");
-    if (ptr) {
-       /* the text following is potentialy a list of email addresses,
-        * so again we check if one of the email addresses is a
-        * substring of ptr
-        */
-       if (strcasestr(ptr, primary)) {
-           return primary;
-       }
-       for (i = 0; i < other_len; i++)
-           if (strcasestr (ptr, other[i])) {
-               return other[i];
-           }
-    }
+
+    /* Note: ptr potentially contains a list of email addresses. */
+    addr = user_address_in_string (ptr, config);
+    if (addr)
+       return addr;
+
     /* Finally, we parse all the " by MTA ..." headers to guess the
      * email address that this was originally delivered to.
      * We extract just the MTA here by removing leading whitespace and
@@ -440,15 +476,11 @@ guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message
             */
            *(tld-1) = '.';
 
-           if (strcasestr(primary, domain)) {
-               free(mta);
-               return primary;
+           addr = string_in_user_address (domain, config);
+           if (addr) {
+               free (mta);
+               return addr;
            }
-           for (i = 0; i < other_len; i++)
-               if (strcasestr (other[i],domain)) {
-                   free(mta);
-                   return other[i];
-               }
        }
        free (mta);
     }
@@ -516,7 +548,8 @@ notmuch_reply_format_default(void *ctx,
                             notmuch_config_t *config,
                             notmuch_query_t *query,
                             notmuch_show_params_t *params,
-                            notmuch_bool_t reply_all)
+                            notmuch_bool_t reply_all,
+                            unused (sprinter_t *sp))
 {
     GMimeMessage *reply;
     notmuch_messages_t *messages;
@@ -544,8 +577,7 @@ notmuch_reply_format_default(void *ctx,
        g_object_unref (G_OBJECT (reply));
        reply = NULL;
 
-       if (mime_node_open (ctx, message, params->cryptoctx, params->decrypt,
-                           &root) == NOTMUCH_STATUS_SUCCESS) {
+       if (mime_node_open (ctx, message, &(params->crypto), &root) == NOTMUCH_STATUS_SUCCESS) {
            format_part_reply (root);
            talloc_free (root);
        }
@@ -556,11 +588,12 @@ notmuch_reply_format_default(void *ctx,
 }
 
 static int
-notmuch_reply_format_json(void *ctx,
-                         notmuch_config_t *config,
-                         notmuch_query_t *query,
-                         notmuch_show_params_t *params,
-                         notmuch_bool_t reply_all)
+notmuch_reply_format_sprinter(void *ctx,
+                             notmuch_config_t *config,
+                             notmuch_query_t *query,
+                             notmuch_show_params_t *params,
+                             notmuch_bool_t reply_all,
+                             sprinter_t *sp)
 {
     GMimeMessage *reply;
     notmuch_messages_t *messages;
@@ -574,27 +607,27 @@ notmuch_reply_format_json(void *ctx,
 
     messages = notmuch_query_search_messages (query);
     message = notmuch_messages_get (messages);
-    if (mime_node_open (ctx, message, params->cryptoctx, params->decrypt,
-                       &node) != NOTMUCH_STATUS_SUCCESS)
+    if (mime_node_open (ctx, message, &(params->crypto), &node) != NOTMUCH_STATUS_SUCCESS)
        return 1;
 
     reply = create_reply_message (ctx, config, message, reply_all);
     if (!reply)
        return 1;
 
+    sp->begin_map (sp);
+
     /* The headers of the reply message we've created */
-    printf ("{\"reply-headers\": ");
-    format_headers_json (ctx, reply, TRUE);
+    sp->map_key (sp, "reply-headers");
+    format_headers_sprinter (sp, reply, TRUE);
     g_object_unref (G_OBJECT (reply));
     reply = NULL;
 
     /* Start the original */
-    printf (", \"original\": ");
-
-    format_part_json (ctx, node, TRUE);
+    sp->map_key (sp, "original");
+    format_part_sprinter (ctx, sp, node, TRUE, TRUE);
 
     /* End */
-    printf ("}\n");
+    sp->end (sp);
     notmuch_message_destroy (message);
 
     return 0;
@@ -606,7 +639,8 @@ notmuch_reply_format_headers_only(void *ctx,
                                  notmuch_config_t *config,
                                  notmuch_query_t *query,
                                  unused (notmuch_show_params_t *params),
-                                 notmuch_bool_t reply_all)
+                                 notmuch_bool_t reply_all,
+                                 unused (sprinter_t *sp))
 {
     GMimeMessage *reply;
     notmuch_messages_t *messages;
@@ -663,6 +697,7 @@ notmuch_reply_format_headers_only(void *ctx,
 enum {
     FORMAT_DEFAULT,
     FORMAT_JSON,
+    FORMAT_SEXP,
     FORMAT_HEADERS_ONLY,
 };
 
@@ -674,22 +709,36 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_string;
     int opt_index, ret = 0;
-    int (*reply_format_func)(void *ctx, notmuch_config_t *config, notmuch_query_t *query, notmuch_show_params_t *params, notmuch_bool_t reply_all);
-    notmuch_show_params_t params = { .part = -1 };
+    int (*reply_format_func) (void *ctx,
+                             notmuch_config_t *config,
+                             notmuch_query_t *query,
+                             notmuch_show_params_t *params,
+                             notmuch_bool_t reply_all,
+                             struct sprinter *sp);
+    notmuch_show_params_t params = {
+       .part = -1,
+       .crypto = {
+           .verify = FALSE,
+           .decrypt = FALSE
+       }
+    };
     int format = FORMAT_DEFAULT;
     int reply_all = TRUE;
+    struct sprinter *sp = NULL;
 
     notmuch_opt_desc_t options[] = {
        { NOTMUCH_OPT_KEYWORD, &format, "format", 'f',
          (notmuch_keyword_t []){ { "default", FORMAT_DEFAULT },
                                  { "json", FORMAT_JSON },
+                                 { "sexp", FORMAT_SEXP },
                                  { "headers-only", FORMAT_HEADERS_ONLY },
                                  { 0, 0 } } },
+       { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 },
        { NOTMUCH_OPT_KEYWORD, &reply_all, "reply-to", 'r',
          (notmuch_keyword_t []){ { "all", TRUE },
                                  { "sender", FALSE },
                                  { 0, 0 } } },
-       { NOTMUCH_OPT_BOOLEAN, &params.decrypt, "decrypt", 'd', 0 },
+       { NOTMUCH_OPT_BOOLEAN, &params.crypto.decrypt, "decrypt", 'd', 0 },
        { 0, 0, 0, 0, 0 }
     };
 
@@ -699,32 +748,20 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
        return 1;
     }
 
-    if (format == FORMAT_HEADERS_ONLY)
+    if (format == FORMAT_HEADERS_ONLY) {
        reply_format_func = notmuch_reply_format_headers_only;
-    else if (format == FORMAT_JSON)
-       reply_format_func = notmuch_reply_format_json;
-    else
+    } else if (format == FORMAT_JSON) {
+       reply_format_func = notmuch_reply_format_sprinter;
+       sp = sprinter_json_create (ctx, stdout);
+    } else if (format == FORMAT_SEXP) {
+       reply_format_func = notmuch_reply_format_sprinter;
+       sp = sprinter_sexp_create (ctx, stdout);
+    } else {
        reply_format_func = notmuch_reply_format_default;
-
-    if (params.decrypt) {
-#ifdef GMIME_ATLEAST_26
-       /* TODO: GMimePasswordRequestFunc */
-       params.cryptoctx = g_mime_gpg_context_new (NULL, "gpg");
-#else
-       GMimeSession* session = g_object_new (g_mime_session_get_type(), NULL);
-       params.cryptoctx = g_mime_gpg_context_new (session, "gpg");
-#endif
-       if (params.cryptoctx) {
-           g_mime_gpg_context_set_always_trust ((GMimeGpgContext*) params.cryptoctx, FALSE);
-       } else {
-           params.decrypt = FALSE;
-           fprintf (stderr, "Failed to construct gpg context.\n");
-       }
-#ifndef GMIME_ATLEAST_26
-       g_object_unref (session);
-#endif
     }
 
+    notmuch_exit_if_unsupported_format ();
+
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
        return 1;
@@ -750,14 +787,12 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
        return 1;
     }
 
-    if (reply_format_func (ctx, config, query, &params, reply_all) != 0)
+    if (reply_format_func (ctx, config, query, &params, reply_all, sp) != 0)
        return 1;
 
+    notmuch_crypto_cleanup (&params.crypto);
     notmuch_query_destroy (query);
     notmuch_database_destroy (notmuch);
 
-    if (params.cryptoctx)
-       g_object_unref(params.cryptoctx);
-
     return ret;
 }
index 4f4096ed15c79b11d06b97abfaa0be0b8fc2af5c..cf26a423705fad8aee399e070416fbfb6c28425e 100644 (file)
  */
 
 #include "notmuch-client.h"
+#include "dump-restore-private.h"
+#include "tag-util.h"
+#include "string-util.h"
 
+static regex_t regex;
+
+/* Non-zero return indicates an error in retrieving the message,
+ * or in applying the tags.  Missing messages are reported, but not
+ * considered errors.
+ */
 static int
-tag_message (notmuch_database_t *notmuch, const char *message_id,
-            char *file_tags, notmuch_bool_t remove_all,
-            notmuch_bool_t synchronize_flags)
+tag_message (unused (void *ctx),
+            notmuch_database_t *notmuch,
+            const char *message_id,
+            tag_op_list_t *tag_ops,
+            tag_op_flag_t flags)
 {
     notmuch_status_t status;
-    notmuch_tags_t *db_tags;
-    char *db_tags_str;
     notmuch_message_t *message = NULL;
-    const char *tag;
-    char *next;
     int ret = 0;
 
     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));
+    if (status) {
+       fprintf (stderr, "Error applying tags to message %s: %s\n",
+                message_id, notmuch_status_to_string (status));
        return 1;
     }
+    if (message == NULL) {
+       fprintf (stderr, "Warning: cannot apply tags to missing message: %s\n",
+                message_id);
+       /* We consider this a non-fatal error. */
+       return 0;
+    }
 
     /* In order to detect missing messages, this check/optimization is
      * intentionally done *after* first finding the message. */
-    if (!remove_all && (file_tags == NULL || *file_tags == '\0'))
-       goto DONE;
-
-    db_tags_str = NULL;
-    for (db_tags = notmuch_message_get_tags (message);
-        notmuch_tags_valid (db_tags);
-        notmuch_tags_move_to_next (db_tags)) {
-       tag = notmuch_tags_get (db_tags);
-
-       if (db_tags_str)
-           db_tags_str = talloc_asprintf_append (db_tags_str, " %s", tag);
-       else
-           db_tags_str = talloc_strdup (message, tag);
-    }
+    if ((flags & TAG_FLAG_REMOVE_ALL) || tag_op_list_size (tag_ops))
+       ret = tag_op_list_apply (message, tag_ops, flags);
 
-    if (((file_tags == NULL || *file_tags == '\0') &&
-        (db_tags_str == NULL || *db_tags_str == '\0')) ||
-       (file_tags && db_tags_str && strcmp (file_tags, db_tags_str) == 0))
-       goto DONE;
+    notmuch_message_destroy (message);
 
-    notmuch_message_freeze (message);
+    return ret;
+}
 
-    if (remove_all)
-       notmuch_message_remove_all_tags (message);
+/* Sup 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 ')'. */
 
-    next = file_tags;
-    while (next) {
-       tag = strsep (&next, " ");
-       if (*tag == '\0')
-           continue;
-       status = notmuch_message_add_tag (message, tag);
-       if (status) {
-           fprintf (stderr, "Error applying tag %s to message %s:\n",
-                    tag, message_id);
-           fprintf (stderr, "%s\n", notmuch_status_to_string (status));
-           ret = 1;
-       }
+static int
+parse_sup_line (void *ctx, char *line,
+               char **query_str, tag_op_list_t *tag_ops)
+{
+
+    regmatch_t match[3];
+    char *file_tags;
+    int rerr;
+
+    tag_op_list_reset (tag_ops);
+
+    chomp_newline (line);
+
+    /* Silently ignore blank lines */
+    if (line[0] == '\0') {
+       return 1;
     }
 
-    notmuch_message_thaw (message);
+    rerr = xregexec (&regex, line, 3, match, 0);
+    if (rerr == REG_NOMATCH) {
+       fprintf (stderr, "Warning: Ignoring invalid sup format line: %s\n",
+                line);
+       return 1;
+    }
 
-    if (synchronize_flags)
-       notmuch_message_tags_to_maildir_flags (message);
+    *query_str = talloc_strndup_debug (ctx, line + match[1].rm_so,
+                                      match[1].rm_eo - match[1].rm_so);
 
-DONE:
-    if (message)
-       notmuch_message_destroy (message);
+    file_tags = talloc_strndup_debug (ctx, line + match[2].rm_so,
+                                     match[2].rm_eo - match[2].rm_so);
+
+    char *tok = file_tags;
+    size_t tok_len = 0;
+
+    tag_op_list_reset (tag_ops);
+
+    while ((tok = strtok_len (tok + tok_len, " ", &tok_len)) != NULL) {
+
+       if (*(tok + tok_len) != '\0') {
+           *(tok + tok_len) = '\0';
+           tok_len++;
+       }
+
+       if (tag_op_list_append (tag_ops, tok, FALSE))
+           return -1;
+    }
+
+    return 0;
 
-    return ret;
 }
 
 int
@@ -100,16 +124,20 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
 {
     notmuch_config_t *config;
     notmuch_database_t *notmuch;
-    notmuch_bool_t synchronize_flags;
     notmuch_bool_t accumulate = FALSE;
+    tag_op_flag_t flags = 0;
+    tag_op_list_t *tag_ops;
+
     char *input_file_name = NULL;
     FILE *input = stdin;
     char *line = NULL;
+    void *line_ctx = NULL;
     size_t line_size;
     ssize_t line_len;
-    regex_t regex;
-    int rerr;
+
+    int ret = 0;
     int opt_index;
+    int input_format = DUMP_FORMAT_AUTO;
 
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
@@ -119,10 +147,16 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
                               NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
        return 1;
 
-    synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
+    if (notmuch_config_get_maildir_synchronize_flags (config))
+       flags |= TAG_FLAG_MAILDIR_SYNC;
 
     notmuch_opt_desc_t options[] = {
-       { NOTMUCH_OPT_POSITION, &input_file_name, 0, 0, 0 },
+       { NOTMUCH_OPT_KEYWORD, &input_format, "format", 'f',
+         (notmuch_keyword_t []){ { "auto", DUMP_FORMAT_AUTO },
+                                 { "batch-tag", DUMP_FORMAT_BATCH_TAG },
+                                 { "sup", DUMP_FORMAT_SUP },
+                                 { 0, 0 } } },
+       { NOTMUCH_OPT_STRING, &input_file_name, "input", 'i', 0 },
        { NOTMUCH_OPT_BOOLEAN,  &accumulate, "accumulate", 'a', 0 },
        { 0, 0, 0, 0, 0 }
     };
@@ -134,6 +168,9 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
        return 1;
     }
 
+    if (! accumulate)
+       flags |= TAG_FLAG_REMOVE_ALL;
+
     if (input_file_name) {
        input = fopen (input_file_name, "r");
        if (input == NULL) {
@@ -141,59 +178,107 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
                     input_file_name, strerror (errno));
            return 1;
        }
-       optind++;
     }
 
     if (opt_index < argc) {
        fprintf (stderr,
-        "Cannot read dump from more than one file: %s\n",
-                argv[optind]);
+                "Unused positional parameter: %s\n",
+                argv[opt_index]);
        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 ')'. */
-    if ( xregcomp (&regex,
-                  "^([^ ]+) \\(([^)]*)\\)$",
-                  REG_EXTENDED) )
-       INTERNAL_ERROR("compile time constant regex failed.");
-
-    while ((line_len = getline (&line, &line_size, input)) != -1) {
-       regmatch_t match[3];
-       char *message_id, *file_tags;
-
-       chomp_newline (line);
-
-       rerr = xregexec (&regex, line, 3, match, 0);
-       if (rerr == REG_NOMATCH)
-       {
-           fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
-                    line);
-           continue;
-       }
+    tag_ops = tag_op_list_create (ctx);
+    if (tag_ops == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return 1;
+    }
 
-       message_id = xstrndup (line + match[1].rm_so,
-                              match[1].rm_eo - match[1].rm_so);
-       file_tags = xstrndup (line + match[2].rm_so,
-                             match[2].rm_eo - match[2].rm_so);
+    do {
+       line_len = getline (&line, &line_size, input);
 
-       tag_message (notmuch, message_id, file_tags, !accumulate,
-                    synchronize_flags);
+       /* empty input file not considered an error */
+       if (line_len < 0)
+           return 0;
 
-       free (message_id);
-       free (file_tags);
+    } while ((line_len == 0) ||
+            (line[0] == '#') ||
+            /* the cast is safe because we checked about for line_len < 0 */
+            (strspn (line, " \t\n") == (unsigned)line_len));
+
+    char *p;
+    for (p = line; (input_format == DUMP_FORMAT_AUTO) && *p; p++) {
+       if (*p == '(')
+           input_format = DUMP_FORMAT_SUP;
     }
 
-    regfree (&regex);
+    if (input_format == DUMP_FORMAT_AUTO)
+       input_format = DUMP_FORMAT_BATCH_TAG;
+
+    if (input_format == DUMP_FORMAT_SUP)
+       if ( xregcomp (&regex,
+                      "^([^ ]+) \\(([^)]*)\\)$",
+                      REG_EXTENDED) )
+           INTERNAL_ERROR ("compile time constant regex failed.");
+
+    do {
+       char *query_string, *prefix, *term;
+
+       if (line_ctx != NULL)
+           talloc_free (line_ctx);
+
+       line_ctx = talloc_new (ctx);
+       if (input_format == DUMP_FORMAT_SUP) {
+           ret = parse_sup_line (line_ctx, line, &query_string, tag_ops);
+       } else {
+           ret = parse_tag_line (line_ctx, line, TAG_FLAG_BE_GENEROUS,
+                                 &query_string, tag_ops);
+
+           if (ret == 0) {
+               ret = parse_boolean_term (line_ctx, query_string,
+                                         &prefix, &term);
+               if (ret && errno == EINVAL) {
+                   fprintf (stderr, "Warning: cannot parse query: %s (skipping)\n", query_string);
+                   continue;
+               } else if (ret) {
+                   /* This is more fatal (e.g., out of memory) */
+                   fprintf (stderr, "Error parsing query: %s\n",
+                            strerror (errno));
+                   ret = 1;
+                   break;
+               } else if (strcmp ("id", prefix) != 0) {
+                   fprintf (stderr, "Warning: not an id query: %s (skipping)\n", query_string);
+                   continue;
+               }
+               query_string = term;
+           }
+       }
+
+       if (ret > 0)
+           continue;
+
+       if (ret < 0)
+           break;
+
+       ret = tag_message (line_ctx, notmuch, query_string,
+                          tag_ops, flags);
+       if (ret)
+           break;
+
+    }  while ((line_len = getline (&line, &line_size, input)) != -1);
+
+    if (line_ctx != NULL)
+       talloc_free (line_ctx);
+
+    if (input_format == DUMP_FORMAT_SUP)
+       regfree (&regex);
 
     if (line)
        free (line);
 
     notmuch_database_destroy (notmuch);
+
     if (input != stdin)
        fclose (input);
 
-    return 0;
+    return ret;
 }
index 3be296d8a3f67db106d139c882544f3746eb4d8d..0b0a879e7353e9b739180bf3a2cb2d16a9b9bd45 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 #include "notmuch-client.h"
+#include "sprinter.h"
 
 typedef enum {
     OUTPUT_SUMMARY,
@@ -28,92 +29,6 @@ typedef enum {
     OUTPUT_TAGS
 } output_t;
 
-typedef struct search_format {
-    const char *results_start;
-    const char *item_start;
-    void (*item_id) (const void *ctx,
-                    const char *item_type,
-                    const char *item_id);
-    void (*thread_summary) (const void *ctx,
-                           const char *thread_id,
-                           const time_t date,
-                           const int matched,
-                           const int total,
-                           const char *authors,
-                           const char *subject);
-    const char *tag_start;
-    const char *tag;
-    const char *tag_sep;
-    const char *tag_end;
-    const char *item_sep;
-    const char *item_end;
-    const char *results_end;
-    const char *results_null;
-} search_format_t;
-
-static void
-format_item_id_text (const void *ctx,
-                    const char *item_type,
-                    const char *item_id);
-
-static void
-format_thread_text (const void *ctx,
-                   const char *thread_id,
-                   const time_t date,
-                   const int matched,
-                   const int total,
-                   const char *authors,
-                   const char *subject);
-static const search_format_t format_text = {
-    "",
-       "",
-           format_item_id_text,
-           format_thread_text,
-           " (",
-               "%s", " ",
-           ")", "\n",
-       "",
-    "\n",
-    "",
-};
-
-static void
-format_item_id_json (const void *ctx,
-                    const char *item_type,
-                    const char *item_id);
-
-static void
-format_thread_json (const void *ctx,
-                   const char *thread_id,
-                   const time_t date,
-                   const int matched,
-                   const int total,
-                   const char *authors,
-                   const char *subject);
-
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
-static const search_format_t format_json = {
-    "[",
-       "{",
-           format_item_id_json,
-           format_thread_json,
-           "\"tags\": [",
-               "\"%s\"", ", ",
-           "]", ",\n",
-       "}",
-    "]\n",
-    "]\n",
-};
-
-static void
-format_item_id_text (unused (const void *ctx),
-                    const char *item_type,
-                    const char *item_id)
-{
-    printf ("%s%s", item_type, item_id);
-}
-
 static char *
 sanitize_string (const void *ctx, const char *str)
 {
@@ -131,72 +46,8 @@ sanitize_string (const void *ctx, const char *str)
     return out;
 }
 
-static void
-format_thread_text (const void *ctx,
-                   const char *thread_id,
-                   const time_t date,
-                   const int matched,
-                   const int total,
-                   const char *authors,
-                   const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("thread:%s %12s [%d/%d] %s; %s",
-           thread_id,
-           notmuch_time_relative_date (ctx, date),
-           matched,
-           total,
-           sanitize_string (ctx_quote, authors),
-           sanitize_string (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
-static void
-format_item_id_json (const void *ctx,
-                    unused (const char *item_type),
-                    const char *item_id)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("%s", json_quote_str (ctx_quote, item_id));
-
-    talloc_free (ctx_quote);
-    
-}
-
-static void
-format_thread_json (const void *ctx,
-                   const char *thread_id,
-                   const time_t date,
-                   const int matched,
-                   const int total,
-                   const char *authors,
-                   const char *subject)
-{
-    void *ctx_quote = talloc_new (ctx);
-
-    printf ("\"thread\": %s,\n"
-           "\"timestamp\": %ld,\n"
-           "\"date_relative\": \"%s\",\n"
-           "\"matched\": %d,\n"
-           "\"total\": %d,\n"
-           "\"authors\": %s,\n"
-           "\"subject\": %s,\n",
-           json_quote_str (ctx_quote, thread_id),
-           date,
-           notmuch_time_relative_date (ctx, date),
-           matched,
-           total,
-           json_quote_str (ctx_quote, authors),
-           json_quote_str (ctx_quote, subject));
-
-    talloc_free (ctx_quote);
-}
-
 static int
-do_search_threads (const search_format_t *format,
+do_search_threads (sprinter_t *format,
                   notmuch_query_t *query,
                   notmuch_sort_t sort,
                   output_t output,
@@ -207,7 +58,6 @@ do_search_threads (const search_format_t *format,
     notmuch_threads_t *threads;
     notmuch_tags_t *tags;
     time_t date;
-    int first_thread = 1;
     int i;
 
     if (offset < 0) {
@@ -220,14 +70,12 @@ do_search_threads (const search_format_t *format,
     if (threads == NULL)
        return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     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) {
@@ -235,60 +83,97 @@ do_search_threads (const search_format_t *format,
            continue;
        }
 
-       if (! first_thread)
-           fputs (format->item_sep, stdout);
-
        if (output == OUTPUT_THREADS) {
-           format->item_id (thread, "thread:",
-                            notmuch_thread_get_thread_id (thread));
+           format->set_prefix (format, "thread");
+           format->string (format,
+                           notmuch_thread_get_thread_id (thread));
+           format->separator (format);
        } else { /* output == OUTPUT_SUMMARY */
-           fputs (format->item_start, stdout);
+           void *ctx_quote = talloc_new (thread);
+           const char *authors = notmuch_thread_get_authors (thread);
+           const char *subject = notmuch_thread_get_subject (thread);
+           const char *thread_id = notmuch_thread_get_thread_id (thread);
+           int matched = notmuch_thread_get_matched_messages (thread);
+           int total = notmuch_thread_get_total_messages (thread);
+           const char *relative_date = NULL;
+           notmuch_bool_t first_tag = TRUE;
+
+           format->begin_map (format);
 
            if (sort == NOTMUCH_SORT_OLDEST_FIRST)
                date = notmuch_thread_get_oldest_date (thread);
            else
                date = notmuch_thread_get_newest_date (thread);
 
-           format->thread_summary (thread,
-                                   notmuch_thread_get_thread_id (thread),
-                                   date,
-                                   notmuch_thread_get_matched_messages (thread),
-                                   notmuch_thread_get_total_messages (thread),
-                                   notmuch_thread_get_authors (thread),
-                                   notmuch_thread_get_subject (thread));
+           relative_date = notmuch_time_relative_date (ctx_quote, date);
+
+           if (format->is_text_printer) {
+                /* Special case for the text formatter */
+               printf ("thread:%s %12s [%d/%d] %s; %s (",
+                       thread_id,
+                       relative_date,
+                       matched,
+                       total,
+                       sanitize_string (ctx_quote, authors),
+                       sanitize_string (ctx_quote, subject));
+           } else { /* Structured Output */
+               format->map_key (format, "thread");
+               format->string (format, thread_id);
+               format->map_key (format, "timestamp");
+               format->integer (format, date);
+               format->map_key (format, "date_relative");
+               format->string (format, relative_date);
+               format->map_key (format, "matched");
+               format->integer (format, matched);
+               format->map_key (format, "total");
+               format->integer (format, total);
+               format->map_key (format, "authors");
+               format->string (format, authors);
+               format->map_key (format, "subject");
+               format->string (format, subject);
+           }
+
+           talloc_free (ctx_quote);
 
-           fputs (format->tag_start, stdout);
+           format->map_key (format, "tags");
+           format->begin_list (format);
 
            for (tags = notmuch_thread_get_tags (thread);
                 notmuch_tags_valid (tags);
                 notmuch_tags_move_to_next (tags))
            {
-               if (! first_tag)
-                   fputs (format->tag_sep, stdout);
-               printf (format->tag, notmuch_tags_get (tags));
-               first_tag = 0;
+               const char *tag = notmuch_tags_get (tags);
+
+               if (format->is_text_printer) {
+                  /* Special case for the text formatter */
+                   if (first_tag)
+                       first_tag = FALSE;
+                   else
+                       fputc (' ', stdout);
+                   fputs (tag, stdout);
+               } else { /* Structured Output */
+                   format->string (format, tag);
+               }
            }
 
-           fputs (format->tag_end, stdout);
+           if (format->is_text_printer)
+               printf (")");
 
-           fputs (format->item_end, stdout);
+           format->end (format);
+           format->end (format);
+           format->separator (format);
        }
 
-       first_thread = 0;
-
        notmuch_thread_destroy (thread);
     }
 
-    if (first_thread)
-       fputs (format->results_null, stdout);
-    else
-       fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
-do_search_messages (const search_format_t *format,
+do_search_messages (sprinter_t *format,
                    notmuch_query_t *query,
                    output_t output,
                    int offset,
@@ -297,7 +182,6 @@ do_search_messages (const search_format_t *format,
     notmuch_message_t *message;
     notmuch_messages_t *messages;
     notmuch_filenames_t *filenames;
-    int first_message = 1;
     int i;
 
     if (offset < 0) {
@@ -310,7 +194,7 @@ do_search_messages (const search_format_t *format,
     if (messages == NULL)
        return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (i = 0;
         notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit);
@@ -328,24 +212,17 @@ do_search_messages (const search_format_t *format,
                 notmuch_filenames_valid (filenames);
                 notmuch_filenames_move_to_next (filenames))
            {
-               if (! first_message)
-                   fputs (format->item_sep, stdout);
-
-               format->item_id (message, "",
-                                notmuch_filenames_get (filenames));
-
-               first_message = 0;
+               format->string (format, notmuch_filenames_get (filenames));
+               format->separator (format);
            }
            
            notmuch_filenames_destroy( filenames );
 
        } else { /* output == OUTPUT_MESSAGES */
-           if (! first_message)
-               fputs (format->item_sep, stdout);
-
-           format->item_id (message, "id:",
-                            notmuch_message_get_message_id (message));
-           first_message = 0;
+           format->set_prefix (format, "id");
+           format->string (format,
+                           notmuch_message_get_message_id (message));
+           format->separator (format);
        }
 
        notmuch_message_destroy (message);
@@ -353,23 +230,19 @@ do_search_messages (const search_format_t *format,
 
     notmuch_messages_destroy (messages);
 
-    if (first_message)
-       fputs (format->results_null, stdout);
-    else
-       fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
 
 static int
 do_search_tags (notmuch_database_t *notmuch,
-               const search_format_t *format,
+               sprinter_t *format,
                notmuch_query_t *query)
 {
     notmuch_messages_t *messages = NULL;
     notmuch_tags_t *tags;
     const char *tag;
-    int first_tag = 1;
 
     /* should the following only special case if no excluded terms
      * specified? */
@@ -387,7 +260,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (tags == NULL)
        return 1;
 
-    fputs (format->results_start, stdout);
+    format->begin_list (format);
 
     for (;
         notmuch_tags_valid (tags);
@@ -395,12 +268,9 @@ do_search_tags (notmuch_database_t *notmuch,
     {
        tag = notmuch_tags_get (tags);
 
-       if (! first_tag)
-           fputs (format->item_sep, stdout);
-
-       format->item_id (tags, "", tag);
+       format->string (format, tag);
+       format->separator (format);
 
-       first_tag = 0;
     }
 
     notmuch_tags_destroy (tags);
@@ -408,10 +278,7 @@ do_search_tags (notmuch_database_t *notmuch,
     if (messages)
        notmuch_messages_destroy (messages);
 
-    if (first_tag)
-       fputs (format->results_null, stdout);
-    else
-       fputs (format->results_end, stdout);
+    format->end (format);
 
     return 0;
 }
@@ -430,7 +297,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_t *query;
     char *query_str;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
-    const search_format_t *format = &format_text;
+    sprinter_t *format = NULL;
     int opt_index, ret;
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
@@ -438,8 +305,12 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     int exclude = EXCLUDE_TRUE;
     unsigned int i;
 
-    enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT }
-       format_sel = NOTMUCH_FORMAT_TEXT;
+    enum {
+       NOTMUCH_FORMAT_JSON,
+       NOTMUCH_FORMAT_TEXT,
+       NOTMUCH_FORMAT_TEXT0,
+       NOTMUCH_FORMAT_SEXP
+    } format_sel = NOTMUCH_FORMAT_TEXT;
 
     notmuch_opt_desc_t options[] = {
        { NOTMUCH_OPT_KEYWORD, &sort, "sort", 's',
@@ -448,8 +319,11 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
                                  { 0, 0 } } },
        { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
          (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
+                                 { "sexp", NOTMUCH_FORMAT_SEXP },
                                  { "text", NOTMUCH_FORMAT_TEXT },
+                                 { "text0", NOTMUCH_FORMAT_TEXT0 },
                                  { 0, 0 } } },
+       { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 },
        { NOTMUCH_OPT_KEYWORD, &output, "output", 'o',
          (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
                                  { "threads", OUTPUT_THREADS },
@@ -475,13 +349,28 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     switch (format_sel) {
     case NOTMUCH_FORMAT_TEXT:
-       format = &format_text;
+       format = sprinter_text_create (ctx, stdout);
+       break;
+    case NOTMUCH_FORMAT_TEXT0:
+       if (output == OUTPUT_SUMMARY) {
+           fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
+           return 1;
+       }
+       format = sprinter_text0_create (ctx, stdout);
        break;
     case NOTMUCH_FORMAT_JSON:
-       format = &format_json;
+       format = sprinter_json_create (ctx, stdout);
        break;
+    case NOTMUCH_FORMAT_SEXP:
+       format = sprinter_sexp_create (ctx, stdout);
+       break;
+    default:
+       /* this should never happen */
+       INTERNAL_ERROR("no output format selected");
     }
 
+    notmuch_exit_if_unsupported_format ();
+
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
        return 1;
@@ -546,5 +435,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     notmuch_query_destroy (query);
     notmuch_database_destroy (notmuch);
 
+    talloc_free (format);
+
     return ret;
 }
index 95427d4fd3096f294714b8a1b4fb069f64fc0503..cbfc2d1cb6efb28a4202c5053fd6d22d0ad58b69 100644 (file)
 
 #include "notmuch-client.h"
 #include "gmime-filter-reply.h"
+#include "sprinter.h"
 
 static notmuch_status_t
-format_part_text (const void *ctx, mime_node_t *node,
+format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
                  int indent, const notmuch_show_params_t *params);
 
 static const notmuch_show_format_t format_text = {
+    .new_sprinter = sprinter_text_create,
     .part = format_part_text,
 };
 
 static notmuch_status_t
-format_part_json_entry (const void *ctx, mime_node_t *node,
-                       int indent, const notmuch_show_params_t *params);
+format_part_sprinter_entry (const void *ctx, sprinter_t *sp, mime_node_t *node,
+                           int indent, const notmuch_show_params_t *params);
 
 static const notmuch_show_format_t format_json = {
-    .message_set_start = "[",
-    .part = format_part_json_entry,
-    .message_set_sep = ", ",
-    .message_set_end = "]"
+    .new_sprinter = sprinter_json_create,
+    .part = format_part_sprinter_entry,
+};
+
+static const notmuch_show_format_t format_sexp = {
+    .new_sprinter = sprinter_sexp_create,
+    .part = format_part_sprinter_entry,
 };
 
 static notmuch_status_t
-format_part_mbox (const void *ctx, mime_node_t *node,
+format_part_mbox (const void *ctx, sprinter_t *sp, mime_node_t *node,
                  int indent, const notmuch_show_params_t *params);
 
 static const notmuch_show_format_t format_mbox = {
+    .new_sprinter = sprinter_text_create,
     .part = format_part_mbox,
 };
 
 static notmuch_status_t
-format_part_raw (unused (const void *ctx), mime_node_t *node,
+format_part_raw (unused (const void *ctx), sprinter_t *sp, mime_node_t *node,
                 unused (int indent),
                 unused (const notmuch_show_params_t *params));
 
 static const notmuch_show_format_t format_raw = {
+    .new_sprinter = sprinter_text_create,
     .part = format_part_raw,
 };
 
@@ -103,35 +110,48 @@ _get_one_line_summary (const void *ctx, notmuch_message_t *message)
                            from, relative_date, tags);
 }
 
+/* Emit a sequence of key/value pairs for the metadata of message.
+ * The caller should begin a map before calling this. */
 static void
-format_message_json (const void *ctx, notmuch_message_t *message)
+format_message_sprinter (sprinter_t *sp, notmuch_message_t *message)
 {
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
+
+    void *local = talloc_new (NULL);
     notmuch_tags_t *tags;
-    int first = 1;
-    void *ctx_quote = talloc_new (ctx);
     time_t date;
     const char *relative_date;
 
+    sp->map_key (sp, "id");
+    sp->string (sp, notmuch_message_get_message_id (message));
+
+    sp->map_key (sp, "match");
+    sp->boolean (sp, notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH));
+
+    sp->map_key (sp, "excluded");
+    sp->boolean (sp, notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED));
+
+    sp->map_key (sp, "filename");
+    sp->string (sp, notmuch_message_get_filename (message));
+
+    sp->map_key (sp, "timestamp");
     date = notmuch_message_get_date (message);
-    relative_date = notmuch_time_relative_date (ctx, date);
+    sp->integer (sp, date);
 
-    printf ("\"id\": %s, \"match\": %s, \"excluded\": %s, \"filename\": %s, \"timestamp\": %ld, \"date_relative\": \"%s\", \"tags\": [",
-           json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),
-           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",
-           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? "true" : "false",
-           json_quote_str (ctx_quote, notmuch_message_get_filename (message)),
-           date, relative_date);
+    sp->map_key (sp, "date_relative");
+    relative_date = notmuch_time_relative_date (local, date);
+    sp->string (sp, relative_date);
 
+    sp->map_key (sp, "tags");
+    sp->begin_list (sp);
     for (tags = notmuch_message_get_tags (message);
         notmuch_tags_valid (tags);
         notmuch_tags_move_to_next (tags))
-    {
-         printf("%s%s", first ? "" : ",",
-               json_quote_str (ctx_quote, notmuch_tags_get (tags)));
-         first = 0;
-    }
-    printf("], ");
-    talloc_free (ctx_quote);
+       sp->string (sp, notmuch_tags_get (tags));
+    sp->end (sp);
+
+    talloc_free (local);
 }
 
 /* Extract just the email address from the contents of a From:
@@ -193,48 +213,63 @@ _is_from_line (const char *line)
 }
 
 void
-format_headers_json (const void *ctx, GMimeMessage *message, notmuch_bool_t reply)
+format_headers_sprinter (sprinter_t *sp, GMimeMessage *message,
+                        notmuch_bool_t reply)
 {
-    void *local = talloc_new (ctx);
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
+
     InternetAddressList *recipients;
     const char *recipients_string;
+    const char *reply_to_string;
+
+    sp->begin_map (sp);
+
+    sp->map_key (sp, "Subject");
+    sp->string (sp, g_mime_message_get_subject (message));
+
+    sp->map_key (sp, "From");
+    sp->string (sp, g_mime_message_get_sender (message));
 
-    printf ("{%s: %s",
-           json_quote_str (local, "Subject"),
-           json_quote_str (local, g_mime_message_get_subject (message)));
-    printf (", %s: %s",
-           json_quote_str (local, "From"),
-           json_quote_str (local, 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 (local, "To"),
-               json_quote_str (local, recipients_string));
+    if (recipients_string) {
+       sp->map_key (sp, "To");
+       sp->string (sp, 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 (local, "Cc"),
-               json_quote_str (local, recipients_string));
+    if (recipients_string) {
+       sp->map_key (sp, "Cc");
+       sp->string (sp, recipients_string);
+    }
+
+    recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_BCC);
+    recipients_string = internet_address_list_to_string (recipients, 0);
+    if (recipients_string) {
+       sp->map_key (sp, "Bcc");
+       sp->string (sp, recipients_string);
+    }
+
+    reply_to_string = g_mime_message_get_reply_to (message);
+    if (reply_to_string) {
+       sp->map_key (sp, "Reply-To");
+       sp->string (sp, reply_to_string);
+    }
 
     if (reply) {
-       printf (", %s: %s",
-               json_quote_str (local, "In-reply-to"),
-               json_quote_str (local, g_mime_object_get_header (GMIME_OBJECT (message), "In-reply-to")));
+       sp->map_key (sp, "In-reply-to");
+       sp->string (sp, g_mime_object_get_header (GMIME_OBJECT (message), "In-reply-to"));
 
-       printf (", %s: %s",
-               json_quote_str (local, "References"),
-               json_quote_str (local, g_mime_object_get_header (GMIME_OBJECT (message), "References")));
+       sp->map_key (sp, "References");
+       sp->string (sp, g_mime_object_get_header (GMIME_OBJECT (message), "References"));
     } else {
-       printf (", %s: %s",
-               json_quote_str (local, "Date"),
-               json_quote_str (local, g_mime_message_get_date_as_string (message)));
+       sp->map_key (sp, "Date");
+       sp->string (sp, g_mime_message_get_date_as_string (message));
     }
 
-    printf ("}");
-
-    talloc_free (local);
+    sp->end (sp);
 }
 
 /* Write a MIME text part out to the given stream.
@@ -333,139 +368,146 @@ signer_status_to_string (GMimeSignerStatus x)
 
 #ifdef GMIME_ATLEAST_26
 static void
-format_part_sigstatus_json (mime_node_t *node)
+format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node)
 {
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
+
     GMimeSignatureList *siglist = node->sig_list;
 
-    printf ("[");
+    sp->begin_list (sp);
 
     if (!siglist) {
-       printf ("]");
+       sp->end (sp);
        return;
     }
 
-    void *ctx_quote = talloc_new (NULL);
     int i;
     for (i = 0; i < g_mime_signature_list_length (siglist); i++) {
        GMimeSignature *signature = g_mime_signature_list_get_signature (siglist, i);
 
-       if (i > 0)
-           printf (", ");
-
-       printf ("{");
+       sp->begin_map (sp);
 
        /* status */
        GMimeSignatureStatus status = g_mime_signature_get_status (signature);
-       printf ("\"status\": %s",
-               json_quote_str (ctx_quote,
-                               signature_status_to_string (status)));
+       sp->map_key (sp, "status");
+       sp->string (sp, signature_status_to_string (status));
 
        GMimeCertificate *certificate = g_mime_signature_get_certificate (signature);
        if (status == GMIME_SIGNATURE_STATUS_GOOD) {
-           if (certificate)
-               printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, g_mime_certificate_get_fingerprint (certificate)));
+           if (certificate) {
+               sp->map_key (sp, "fingerprint");
+               sp->string (sp, g_mime_certificate_get_fingerprint (certificate));
+           }
            /* these dates are seconds since the epoch; should we
             * provide a more human-readable format string? */
            time_t created = g_mime_signature_get_created (signature);
-           if (created != -1)
-               printf (", \"created\": %d", (int) created);
+           if (created != -1) {
+               sp->map_key (sp, "created");
+               sp->integer (sp, created);
+           }
            time_t expires = g_mime_signature_get_expires (signature);
-           if (expires > 0)
-               printf (", \"expires\": %d", (int) expires);
+           if (expires > 0) {
+               sp->map_key (sp, "expires");
+               sp->integer (sp, expires);
+           }
            /* output user id only if validity is FULL or ULTIMATE. */
            /* note that gmime is using the term "trust" here, which
             * is WRONG.  It's actually user id "validity". */
            if (certificate) {
                const char *name = g_mime_certificate_get_name (certificate);
                GMimeCertificateTrust trust = g_mime_certificate_get_trust (certificate);
-               if (name && (trust == GMIME_CERTIFICATE_TRUST_FULLY || trust == GMIME_CERTIFICATE_TRUST_ULTIMATE))
-                   printf (", \"userid\": %s", json_quote_str (ctx_quote, name));
+               if (name && (trust == GMIME_CERTIFICATE_TRUST_FULLY || trust == GMIME_CERTIFICATE_TRUST_ULTIMATE)) {
+                   sp->map_key (sp, "userid");
+                   sp->string (sp, name);
+               }
            }
        } else if (certificate) {
            const char *key_id = g_mime_certificate_get_key_id (certificate);
-           if (key_id)
-               printf (", \"keyid\": %s", json_quote_str (ctx_quote, key_id));
+           if (key_id) {
+               sp->map_key (sp, "keyid");
+               sp->string (sp, key_id);
+           }
        }
 
        GMimeSignatureError errors = g_mime_signature_get_errors (signature);
        if (errors != GMIME_SIGNATURE_ERROR_NONE) {
-           printf (", \"errors\": %d", errors);
+           sp->map_key (sp, "errors");
+           sp->integer (sp, errors);
        }
 
-       printf ("}");
+       sp->end (sp);
      }
 
-    printf ("]");
-
-    talloc_free (ctx_quote);
+    sp->end (sp);
 }
 #else
 static void
-format_part_sigstatus_json (mime_node_t *node)
+format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node)
 {
     const GMimeSignatureValidity* validity = node->sig_validity;
 
-    printf ("[");
+    sp->begin_list (sp);
 
     if (!validity) {
-       printf ("]");
+       sp->end (sp);
        return;
     }
 
     const GMimeSigner *signer = g_mime_signature_validity_get_signers (validity);
-    int first = 1;
-    void *ctx_quote = talloc_new (NULL);
-
     while (signer) {
-       if (first)
-           first = 0;
-       else
-           printf (", ");
-
-       printf ("{");
+       sp->begin_map (sp);
 
        /* status */
-       printf ("\"status\": %s",
-               json_quote_str (ctx_quote,
-                               signer_status_to_string (signer->status)));
+       sp->map_key (sp, "status");
+       sp->string (sp, signer_status_to_string (signer->status));
 
        if (signer->status == GMIME_SIGNER_STATUS_GOOD)
        {
-           if (signer->fingerprint)
-               printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, signer->fingerprint));
+           if (signer->fingerprint) {
+               sp->map_key (sp, "fingerprint");
+               sp->string (sp, signer->fingerprint);
+           }
            /* these dates are seconds since the epoch; should we
             * provide a more human-readable format string? */
-           if (signer->created)
-               printf (", \"created\": %d", (int) signer->created);
-           if (signer->expires)
-               printf (", \"expires\": %d", (int) signer->expires);
+           if (signer->created) {
+               sp->map_key (sp, "created");
+               sp->integer (sp, signer->created);
+           }
+           if (signer->expires) {
+               sp->map_key (sp, "expires");
+               sp->integer (sp, signer->expires);
+           }
            /* output user id only if validity is FULL or ULTIMATE. */
            /* note that gmime is using the term "trust" here, which
             * is WRONG.  It's actually user id "validity". */
            if ((signer->name) && (signer->trust)) {
-               if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE))
-                   printf (", \"userid\": %s", json_quote_str (ctx_quote, signer->name));
+               if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE)) {
+                   sp->map_key (sp, "userid");
+                   sp->string (sp, signer->name);
+               }
            }
        } else {
-           if (signer->keyid)
-               printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
+           if (signer->keyid) {
+              sp->map_key (sp, "keyid");
+              sp->string (sp, signer->keyid);
+          }
        }
        if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
-           printf (", \"errors\": %d", signer->errors);
+          sp->map_key (sp, "errors");
+          sp->integer (sp, signer->errors);
        }
 
-       printf ("}");
+       sp->end (sp);
        signer = signer->next;
     }
 
-    printf ("]");
-
-    talloc_free (ctx_quote);
+    sp->end (sp);
 }
 #endif
 
 static notmuch_status_t
-format_part_text (const void *ctx, mime_node_t *node,
+format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
                  int indent, const notmuch_show_params_t *params)
 {
     /* The disposition and content-type metadata are associated with
@@ -547,7 +589,7 @@ format_part_text (const void *ctx, mime_node_t *node,
     }
 
     for (i = 0; i < node->nchildren; i++)
-       format_part_text (ctx, mime_node_child (node, i), indent, params);
+       format_part_text (ctx, sp, mime_node_child (node, i), indent, params);
 
     if (GMIME_IS_MESSAGE (node->part))
        printf ("\fbody}\n");
@@ -557,27 +599,53 @@ format_part_text (const void *ctx, mime_node_t *node,
     return NOTMUCH_STATUS_SUCCESS;
 }
 
+static void
+format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart *part)
+{
+    const char *content_charset = g_mime_object_get_content_type_parameter (meta, "charset");
+    const char *cte = g_mime_object_get_header (meta, "content-transfer-encoding");
+    GMimeDataWrapper *wrapper = g_mime_part_get_content_object (part);
+    GMimeStream *stream = g_mime_data_wrapper_get_stream (wrapper);
+    ssize_t content_length = g_mime_stream_length (stream);
+
+    if (content_charset != NULL) {
+       sp->map_key (sp, "content-charset");
+       sp->string (sp, content_charset);
+    }
+    if (cte != NULL) {
+       sp->map_key (sp, "content-transfer-encoding");
+       sp->string (sp, cte);
+    }
+    if (content_length >= 0) {
+       sp->map_key (sp, "content-length");
+       sp->integer (sp, content_length);
+    }
+}
+
 void
-format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first)
+format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
+                     notmuch_bool_t first, notmuch_bool_t output_body)
 {
-    /* Any changes to the JSON format should be reflected in the file
-     * devel/schemata. */
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
 
     if (node->envelope_file) {
-       printf ("{");
-       format_message_json (ctx, node->envelope_file);
-
-       printf ("\"headers\": ");
-       format_headers_json (ctx, GMIME_MESSAGE (node->part), FALSE);
+       sp->begin_map (sp);
+       format_message_sprinter (sp, node->envelope_file);
 
-       printf (", \"body\": [");
-       format_part_json (ctx, mime_node_child (node, 0), first);
+       sp->map_key (sp, "headers");
+       format_headers_sprinter (sp, GMIME_MESSAGE (node->part), FALSE);
 
-       printf ("]}");
+       if (output_body) {
+           sp->map_key (sp, "body");
+           sp->begin_list (sp);
+           format_part_sprinter (ctx, sp, mime_node_child (node, 0), first, TRUE);
+           sp->end (sp);
+       }
+       sp->end (sp);
        return;
     }
 
-    void *local = talloc_new (ctx);
     /* The disposition and content-type metadata are associated with
      * the envelope for message parts */
     GMimeObject *meta = node->envelope_part ?
@@ -586,31 +654,41 @@ format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first)
     const char *cid = g_mime_object_get_content_id (meta);
     const char *filename = GMIME_IS_PART (node->part) ?
        g_mime_part_get_filename (GMIME_PART (node->part)) : NULL;
-    const char *terminator = "";
+    int nclose = 0;
     int i;
 
-    if (!first)
-       printf (", ");
+    sp->begin_map (sp);
 
-    printf ("{\"id\": %d", node->part_num);
+    sp->map_key (sp, "id");
+    sp->integer (sp, node->part_num);
 
-    if (node->decrypt_attempted)
-       printf (", \"encstatus\": [{\"status\": \"%s\"}]",
-               node->decrypt_success ? "good" : "bad");
+    if (node->decrypt_attempted) {
+       sp->map_key (sp, "encstatus");
+       sp->begin_list (sp);
+       sp->begin_map (sp);
+       sp->map_key (sp, "status");
+       sp->string (sp, node->decrypt_success ? "good" : "bad");
+       sp->end (sp);
+       sp->end (sp);
+    }
 
     if (node->verify_attempted) {
-       printf (", \"sigstatus\": ");
-       format_part_sigstatus_json (node);
+       sp->map_key (sp, "sigstatus");
+       format_part_sigstatus_sprinter (sp, node);
     }
 
-    printf (", \"content-type\": %s",
-           json_quote_str (local, g_mime_content_type_to_string (content_type)));
+    sp->map_key (sp, "content-type");
+    sp->string (sp, g_mime_content_type_to_string (content_type));
 
-    if (cid)
-       printf (", \"content-id\": %s", json_quote_str (local, cid));
+    if (cid) {
+       sp->map_key (sp, "content-id");
+       sp->string (sp, cid);
+    }
 
-    if (filename)
-       printf (", \"filename\": %s", json_quote_str (local, filename));
+    if (filename) {
+       sp->map_key (sp, "filename");
+       sp->string (sp, filename);
+    }
 
     if (GMIME_IS_PART (node->part)) {
        /* For non-HTML text parts, we include the content in the
@@ -622,45 +700,52 @@ format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first)
         * makes charset decoding the responsibility on the caller, we
         * report the charset for text/html parts.
         */
-       if (g_mime_content_type_is_type (content_type, "text", "html")) {
-           const char *content_charset = g_mime_object_get_content_type_parameter (meta, "charset");
-
-           if (content_charset != NULL)
-               printf (", \"content-charset\": %s", json_quote_str (local, content_charset));
-       } else if (g_mime_content_type_is_type (content_type, "text", "*")) {
+       if (g_mime_content_type_is_type (content_type, "text", "*") &&
+           ! g_mime_content_type_is_type (content_type, "text", "html"))
+       {
            GMimeStream *stream_memory = g_mime_stream_mem_new ();
            GByteArray *part_content;
            show_text_part_content (node->part, stream_memory, 0);
            part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
-
-           printf (", \"content\": %s", json_quote_chararray (local, (char *) part_content->data, part_content->len));
+           sp->map_key (sp, "content");
+           sp->string_len (sp, (char *) part_content->data, part_content->len);
            g_object_unref (stream_memory);
+       } else {
+           format_omitted_part_meta_sprinter (sp, meta, GMIME_PART (node->part));
        }
     } else if (GMIME_IS_MULTIPART (node->part)) {
-       printf (", \"content\": [");
-       terminator = "]";
+       sp->map_key (sp, "content");
+       sp->begin_list (sp);
+       nclose = 1;
     } else if (GMIME_IS_MESSAGE (node->part)) {
-       printf (", \"content\": [{");
-       printf ("\"headers\": ");
-       format_headers_json (local, GMIME_MESSAGE (node->part), FALSE);
+       sp->map_key (sp, "content");
+       sp->begin_list (sp);
+       sp->begin_map (sp);
 
-       printf (", \"body\": [");
-       terminator = "]}]";
-    }
+       sp->map_key (sp, "headers");
+       format_headers_sprinter (sp, GMIME_MESSAGE (node->part), FALSE);
 
-    talloc_free (local);
+       sp->map_key (sp, "body");
+       sp->begin_list (sp);
+       nclose = 3;
+    }
 
     for (i = 0; i < node->nchildren; i++)
-       format_part_json (ctx, mime_node_child (node, i), i == 0);
+       format_part_sprinter (ctx, sp, mime_node_child (node, i), i == 0, TRUE);
 
-    printf ("%s}", terminator);
+    /* Close content structures */
+    for (i = 0; i < nclose; i++)
+       sp->end (sp);
+    /* Close part map */
+    sp->end (sp);
 }
 
 static notmuch_status_t
-format_part_json_entry (const void *ctx, mime_node_t *node, unused (int indent),
-                       unused (const notmuch_show_params_t *params))
+format_part_sprinter_entry (const void *ctx, sprinter_t *sp,
+                           mime_node_t *node, unused (int indent),
+                           const notmuch_show_params_t *params)
 {
-    format_part_json (ctx, node, TRUE);
+    format_part_sprinter (ctx, sp, node, TRUE, params->output_body);
 
     return NOTMUCH_STATUS_SUCCESS;
 }
@@ -671,7 +756,8 @@ format_part_json_entry (const void *ctx, mime_node_t *node, unused (int indent),
  * http://qmail.org/qmail-manual-html/man5/mbox.html
  */
 static notmuch_status_t
-format_part_mbox (const void *ctx, mime_node_t *node, unused (int indent),
+format_part_mbox (const void *ctx, unused (sprinter_t *sp), mime_node_t *node,
+                 unused (int indent),
                  unused (const notmuch_show_params_t *params))
 {
     notmuch_message_t *message = node->envelope_file;
@@ -722,8 +808,8 @@ format_part_mbox (const void *ctx, mime_node_t *node, unused (int indent),
 }
 
 static notmuch_status_t
-format_part_raw (unused (const void *ctx), mime_node_t *node,
-                unused (int indent),
+format_part_raw (unused (const void *ctx), unused (sprinter_t *sp),
+                mime_node_t *node, unused (int indent),
                 unused (const notmuch_show_params_t *params))
 {
     if (node->envelope_file) {
@@ -802,6 +888,7 @@ format_part_raw (unused (const void *ctx), mime_node_t *node,
 static notmuch_status_t
 show_message (void *ctx,
              const notmuch_show_format_t *format,
+             sprinter_t *sp,
              notmuch_message_t *message,
              int indent,
              notmuch_show_params_t *params)
@@ -810,13 +897,12 @@ show_message (void *ctx,
     mime_node_t *root, *part;
     notmuch_status_t status;
 
-    status = mime_node_open (local, message, params->cryptoctx,
-                            params->decrypt, &root);
+    status = mime_node_open (local, message, &(params->crypto), &root);
     if (status)
        goto DONE;
     part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
     if (part)
-       status = format->part (local, part, indent, params);
+       status = format->part (local, sp, part, indent, params);
   DONE:
     talloc_free (local);
     return status;
@@ -825,6 +911,7 @@ show_message (void *ctx,
 static notmuch_status_t
 show_messages (void *ctx,
               const notmuch_show_format_t *format,
+              sprinter_t *sp,
               notmuch_messages_t *messages,
               int indent,
               notmuch_show_params_t *params)
@@ -832,23 +919,16 @@ show_messages (void *ctx,
     notmuch_message_t *message;
     notmuch_bool_t match;
     notmuch_bool_t excluded;
-    int first_set = 1;
     int next_indent;
     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
 
-    if (format->message_set_start)
-       fputs (format->message_set_start, stdout);
+    sp->begin_list (sp);
 
     for (;
         notmuch_messages_valid (messages);
         notmuch_messages_move_to_next (messages))
     {
-       if (!first_set && format->message_set_sep)
-           fputs (format->message_set_sep, stdout);
-       first_set = 0;
-
-       if (format->message_set_start)
-           fputs (format->message_set_start, stdout);
+       sp->begin_list (sp);
 
        message = notmuch_messages_get (messages);
 
@@ -858,17 +938,16 @@ show_messages (void *ctx,
        next_indent = indent;
 
        if ((match && (!excluded || !params->omit_excluded)) || params->entire_thread) {
-           status = show_message (ctx, format, message, indent, params);
+           status = show_message (ctx, format, sp, message, indent, params);
            if (status && !res)
                res = status;
            next_indent = indent + 1;
-
-           if (!status && format->message_set_sep)
-               fputs (format->message_set_sep, stdout);
+       } else {
+           sp->null (sp);
        }
 
        status = show_messages (ctx,
-                               format,
+                               format, sp,
                                notmuch_message_get_replies (message),
                                next_indent,
                                params);
@@ -877,12 +956,10 @@ show_messages (void *ctx,
 
        notmuch_message_destroy (message);
 
-       if (format->message_set_end)
-           fputs (format->message_set_end, stdout);
+       sp->end (sp);
     }
 
-    if (format->message_set_end)
-       fputs (format->message_set_end, stdout);
+    sp->end (sp);
 
     return res;
 }
@@ -892,6 +969,7 @@ static int
 do_show_single (void *ctx,
                notmuch_query_t *query,
                const notmuch_show_format_t *format,
+               sprinter_t *sp,
                notmuch_show_params_t *params)
 {
     notmuch_messages_t *messages;
@@ -912,7 +990,8 @@ do_show_single (void *ctx,
 
     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
 
-    return show_message (ctx, format, message, 0, params) != NOTMUCH_STATUS_SUCCESS;
+    return show_message (ctx, format, sp, message, 0, params)
+       != NOTMUCH_STATUS_SUCCESS;
 }
 
 /* Formatted output of threads */
@@ -920,16 +999,15 @@ static int
 do_show (void *ctx,
         notmuch_query_t *query,
         const notmuch_show_format_t *format,
+        sprinter_t *sp,
         notmuch_show_params_t *params)
 {
     notmuch_threads_t *threads;
     notmuch_thread_t *thread;
     notmuch_messages_t *messages;
-    int first_toplevel = 1;
     notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
 
-    if (format->message_set_start)
-       fputs (format->message_set_start, stdout);
+    sp->begin_list (sp);
 
     for (threads = notmuch_query_search_threads (query);
         notmuch_threads_valid (threads);
@@ -943,11 +1021,7 @@ do_show (void *ctx,
            INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
                            notmuch_thread_get_thread_id (thread));
 
-       if (!first_toplevel && format->message_set_sep)
-           fputs (format->message_set_sep, stdout);
-       first_toplevel = 0;
-
-       status = show_messages (ctx, format, messages, 0, params);
+       status = show_messages (ctx, format, sp, messages, 0, params);
        if (status && !res)
            res = status;
 
@@ -955,8 +1029,7 @@ do_show (void *ctx,
 
     }
 
-    if (format->message_set_end)
-       fputs (format->message_set_end, stdout);
+    sp->end (sp);
 
     return res != NOTMUCH_STATUS_SUCCESS;
 }
@@ -964,11 +1037,18 @@ do_show (void *ctx,
 enum {
     NOTMUCH_FORMAT_NOT_SPECIFIED,
     NOTMUCH_FORMAT_JSON,
+    NOTMUCH_FORMAT_SEXP,
     NOTMUCH_FORMAT_TEXT,
     NOTMUCH_FORMAT_MBOX,
     NOTMUCH_FORMAT_RAW
 };
 
+enum {
+    ENTIRE_THREAD_DEFAULT,
+    ENTIRE_THREAD_TRUE,
+    ENTIRE_THREAD_FALSE,
+};
+
 /* The following is to allow future options to be added more easily */
 enum {
     EXCLUDE_TRUE,
@@ -984,26 +1064,42 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
     char *query_string;
     int opt_index, ret;
     const notmuch_show_format_t *format = &format_text;
-    notmuch_show_params_t params = { .part = -1, .omit_excluded = TRUE };
+    sprinter_t *sprinter;
+    notmuch_show_params_t params = {
+       .part = -1,
+       .omit_excluded = TRUE,
+       .output_body = TRUE,
+       .crypto = {
+           .verify = FALSE,
+           .decrypt = FALSE
+       }
+    };
     int format_sel = NOTMUCH_FORMAT_NOT_SPECIFIED;
-    notmuch_bool_t verify = FALSE;
     int exclude = EXCLUDE_TRUE;
+    int entire_thread = ENTIRE_THREAD_DEFAULT;
 
     notmuch_opt_desc_t options[] = {
        { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
          (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
                                  { "text", NOTMUCH_FORMAT_TEXT },
+                                 { "sexp", NOTMUCH_FORMAT_SEXP },
                                  { "mbox", NOTMUCH_FORMAT_MBOX },
                                  { "raw", NOTMUCH_FORMAT_RAW },
                                  { 0, 0 } } },
-        { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
-          (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
-                                  { "false", EXCLUDE_FALSE },
-                                  { 0, 0 } } },
+       { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 },
+       { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
+         (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
+                                 { "false", EXCLUDE_FALSE },
+                                 { 0, 0 } } },
+       { NOTMUCH_OPT_KEYWORD, &entire_thread, "entire-thread", 't',
+         (notmuch_keyword_t []){ { "true", ENTIRE_THREAD_TRUE },
+                                 { "false", ENTIRE_THREAD_FALSE },
+                                 { "", ENTIRE_THREAD_TRUE },
+                                 { 0, 0 } } },
        { NOTMUCH_OPT_INT, &params.part, "part", 'p', 0 },
-       { NOTMUCH_OPT_BOOLEAN, &params.entire_thread, "entire-thread", 't', 0 },
-       { NOTMUCH_OPT_BOOLEAN, &params.decrypt, "decrypt", 'd', 0 },
-       { NOTMUCH_OPT_BOOLEAN, &verify, "verify", 'v', 0 },
+       { NOTMUCH_OPT_BOOLEAN, &params.crypto.decrypt, "decrypt", 'd', 0 },
+       { NOTMUCH_OPT_BOOLEAN, &params.crypto.verify, "verify", 'v', 0 },
+       { NOTMUCH_OPT_BOOLEAN, &params.output_body, "body", 'b', 0 },
        { 0, 0, 0, 0, 0 }
     };
 
@@ -1013,6 +1109,10 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        return 1;
     }
 
+    /* decryption implies verification */
+    if (params.crypto.decrypt)
+       params.crypto.verify = TRUE;
+
     if (format_sel == NOTMUCH_FORMAT_NOT_SPECIFIED) {
        /* if part was requested and format was not specified, use format=raw */
        if (params.part >= 0)
@@ -1024,11 +1124,13 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
     switch (format_sel) {
     case NOTMUCH_FORMAT_JSON:
        format = &format_json;
-       params.entire_thread = TRUE;
        break;
     case NOTMUCH_FORMAT_TEXT:
        format = &format_text;
        break;
+    case NOTMUCH_FORMAT_SEXP:
+       format = &format_sexp;
+       break;
     case NOTMUCH_FORMAT_MBOX:
        if (params.part > 0) {
            fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
@@ -1047,25 +1149,33 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        break;
     }
 
-    if (params.decrypt || verify) {
-#ifdef GMIME_ATLEAST_26
-       /* TODO: GMimePasswordRequestFunc */
-       params.cryptoctx = g_mime_gpg_context_new (NULL, "gpg");
-#else
-       GMimeSession* session = g_object_new (g_mime_session_get_type(), NULL);
-       params.cryptoctx = g_mime_gpg_context_new (session, "gpg");
-#endif
-       if (params.cryptoctx) {
-           g_mime_gpg_context_set_always_trust ((GMimeGpgContext*) params.cryptoctx, FALSE);
+    notmuch_exit_if_unsupported_format ();
+
+    /* Default is entire-thread = FALSE except for format=json and
+     * format=sexp. */
+    if (entire_thread == ENTIRE_THREAD_DEFAULT) {
+       if (format == &format_json || format == &format_sexp)
+           entire_thread = ENTIRE_THREAD_TRUE;
+       else
+           entire_thread = ENTIRE_THREAD_FALSE;
+    }
+
+    if (!params.output_body) {
+       if (params.part > 0) {
+           fprintf (stderr, "Warning: --body=false is incompatible with --part > 0. Disabling.\n");
+           params.output_body = TRUE;
        } else {
-           params.decrypt = FALSE;
-           fprintf (stderr, "Failed to construct gpg context.\n");
+           if (format != &format_json && format != &format_sexp)
+               fprintf (stderr,
+                        "Warning: --body=false only implemented for format=json and format=sexp\n");
        }
-#ifndef GMIME_ATLEAST_26
-       g_object_unref (session);
-#endif
     }
 
+    if (entire_thread == ENTIRE_THREAD_TRUE)
+       params.entire_thread = TRUE;
+    else
+       params.entire_thread = FALSE;
+
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
        return 1;
@@ -1091,9 +1201,12 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        return 1;
     }
 
+    /* Create structure printer. */
+    sprinter = format->new_sprinter(ctx, stdout);
+
     /* If a single message is requested we do not use search_excludes. */
     if (params.part >= 0)
-       ret = do_show_single (ctx, query, format, &params);
+       ret = do_show_single (ctx, query, format, sprinter, &params);
     else {
        /* We always apply set the exclude flag. The
         * exclude=true|false option controls whether or not we return
@@ -1112,14 +1225,12 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
            params.omit_excluded = FALSE;
        }
 
-       ret = do_show (ctx, query, format, &params);
+       ret = do_show (ctx, query, format, sprinter, &params);
     }
 
+    notmuch_crypto_cleanup (&params.crypto);
     notmuch_query_destroy (query);
     notmuch_database_destroy (notmuch);
 
-    if (params.cryptoctx)
-       g_object_unref(params.cryptoctx);
-
     return ret;
 }
index 7d186399ba786149d607ca2b45837836a43af802..b54c55dd71428ebca229f6135d57a78e109f8e00 100644 (file)
@@ -19,6 +19,8 @@
  */
 
 #include "notmuch-client.h"
+#include "tag-util.h"
+#include "string-util.h"
 
 static volatile sig_atomic_t interrupted;
 
@@ -31,36 +33,14 @@ handle_sigint (unused (int sig))
      * result.  It is not required for correctness, and if it does
      * fail or produce a short write, we want to get out of the signal
      * handler as quickly as possible, not retry it. */
-    IGNORE_RESULT (write (2, msg, sizeof(msg)-1));
+    IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));
     interrupted = 1;
 }
 
-static char *
-_escape_tag (char *buf, const char *tag)
-{
-    const char *in = tag;
-    char *out = buf;
-    /* Boolean terms surrounded by double quotes can contain any
-     * character.  Double quotes are quoted by doubling them. */
-    *out++ = '"';
-    while (*in) {
-       if (*in == '"')
-           *out++ = '"';
-       *out++ = *in++;
-    }
-    *out++ = '"';
-    *out = 0;
-    return buf;
-}
-
-typedef struct {
-    const char *tag;
-    notmuch_bool_t remove;
-} tag_operation_t;
 
 static char *
 _optimize_tag_query (void *ctx, const char *orig_query_string,
-                    const tag_operation_t *tag_ops)
+                    const tag_op_list_t *list)
 {
     /* This is subtler than it looks.  Xapian ignores the '-' operator
      * at the beginning both queries and parenthesized groups and,
@@ -70,36 +50,34 @@ _optimize_tag_query (void *ctx, const char *orig_query_string,
      * parenthesize and the exclusion part of the query must not use
      * the '-' operator (though the NOT operator is fine). */
 
-    char *escaped, *query_string;
+    char *escaped = NULL;
+    size_t escaped_len = 0;
+    char *query_string;
     const char *join = "";
-    int i;
-    unsigned int max_tag_len = 0;
+    size_t i;
 
     /* Don't optimize if there are no tag changes. */
-    if (tag_ops[0].tag == NULL)
+    if (tag_op_list_size (list) == 0)
        return talloc_strdup (ctx, orig_query_string);
 
-    /* Allocate a buffer for escaping tags.  This is large enough to
-     * hold a fully escaped tag with every character doubled plus
-     * enclosing quotes and a NUL. */
-    for (i = 0; tag_ops[i].tag; i++)
-       if (strlen (tag_ops[i].tag) > max_tag_len)
-           max_tag_len = strlen (tag_ops[i].tag);
-    escaped = talloc_array(ctx, char, max_tag_len * 2 + 3);
-    if (!escaped)
-       return NULL;
-
     /* Build the new query string */
     if (strcmp (orig_query_string, "*") == 0)
        query_string = talloc_strdup (ctx, "(");
     else
        query_string = talloc_asprintf (ctx, "( %s ) and (", orig_query_string);
 
-    for (i = 0; tag_ops[i].tag && query_string; i++) {
+    for (i = 0; i < tag_op_list_size (list) && query_string; i++) {
+       /* XXX in case of OOM, query_string will be deallocated when
+        * ctx is, which might be at shutdown */
+       if (make_boolean_term (ctx,
+                              "tag", tag_op_list_tag (list, i),
+                              &escaped, &escaped_len))
+           return NULL;
+
        query_string = talloc_asprintf_append_buffer (
-           query_string, "%s%stag:%s", join,
-           tag_ops[i].remove ? "" : "not ",
-           _escape_tag (escaped, tag_ops[i].tag));
+           query_string, "%s%s%s", join,
+           tag_op_list_isremove (list, i) ? "" : "not ",
+           escaped);
        join = " or ";
     }
 
@@ -110,17 +88,16 @@ _optimize_tag_query (void *ctx, const char *orig_query_string,
     return query_string;
 }
 
-/* Tag messages matching 'query_string' according to 'tag_ops', which
- * must be an array of tagging operations terminated with an empty
- * element. */
+/* Tag messages matching 'query_string' according to 'tag_ops'
+ */
 static int
 tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string,
-          tag_operation_t *tag_ops, notmuch_bool_t synchronize_flags)
+          tag_op_list_t *tag_ops, tag_op_flag_t flags)
 {
     notmuch_query_t *query;
     notmuch_messages_t *messages;
     notmuch_message_t *message;
-    int i;
+    int ret = 0;
 
     /* Optimize the query so it excludes messages that already have
      * the specified set of tags. */
@@ -140,45 +117,80 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string,
     notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
 
     for (messages = notmuch_query_search_messages (query);
-        notmuch_messages_valid (messages) && !interrupted;
-        notmuch_messages_move_to_next (messages))
-    {
+        notmuch_messages_valid (messages) && ! interrupted;
+        notmuch_messages_move_to_next (messages)) {
        message = notmuch_messages_get (messages);
+       ret = tag_op_list_apply (message, tag_ops, flags | TAG_FLAG_PRE_OPTIMIZED);
+       notmuch_message_destroy (message);
+       if (ret != NOTMUCH_STATUS_SUCCESS)
+           break;
+    }
 
-       notmuch_message_freeze (message);
+    notmuch_query_destroy (query);
 
-       for (i = 0; tag_ops[i].tag; i++) {
-           if (tag_ops[i].remove)
-               notmuch_message_remove_tag (message, tag_ops[i].tag);
-           else
-               notmuch_message_add_tag (message, tag_ops[i].tag);
-       }
+    return ret || interrupted;
+}
 
-       notmuch_message_thaw (message);
+static int
+tag_file (void *ctx, notmuch_database_t *notmuch, tag_op_flag_t flags,
+         FILE *input)
+{
+    char *line = NULL;
+    char *query_string = NULL;
+    size_t line_size = 0;
+    ssize_t line_len;
+    int ret = 0;
+    int warn = 0;
+    tag_op_list_t *tag_ops;
+
+    tag_ops = tag_op_list_create (ctx);
+    if (tag_ops == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return 1;
+    }
 
-       if (synchronize_flags)
-           notmuch_message_tags_to_maildir_flags (message);
+    while ((line_len = getline (&line, &line_size, input)) != -1 &&
+          ! interrupted) {
 
-       notmuch_message_destroy (message);
+       ret = parse_tag_line (ctx, line, TAG_FLAG_NONE,
+                             &query_string, tag_ops);
+
+       if (ret > 0) {
+           if (ret != TAG_PARSE_SKIPPED)
+               /* remember there has been problematic lines */
+               warn = 1;
+           ret = 0;
+           continue;
+       }
+
+       if (ret < 0)
+           break;
+
+       ret = tag_query (ctx, notmuch, query_string, tag_ops, flags);
+       if (ret)
+           break;
     }
 
-    notmuch_query_destroy (query);
+    if (line)
+       free (line);
 
-    return interrupted;
+    return ret || warn;
 }
 
 int
 notmuch_tag_command (void *ctx, int argc, char *argv[])
 {
-    tag_operation_t *tag_ops;
-    int tag_ops_count = 0;
-    char *query_string;
+    tag_op_list_t *tag_ops = NULL;
+    char *query_string = NULL;
     notmuch_config_t *config;
     notmuch_database_t *notmuch;
     struct sigaction action;
-    notmuch_bool_t synchronize_flags;
-    int i;
-    int ret;
+    tag_op_flag_t tag_flags = TAG_FLAG_NONE;
+    notmuch_bool_t batch = FALSE;
+    FILE *input = stdin;
+    char *input_file_name = NULL;
+    int opt_index;
+    int ret = 0;
 
     /* Setup our handler for SIGINT */
     memset (&action, 0, sizeof (struct sigaction));
@@ -187,42 +199,41 @@ notmuch_tag_command (void *ctx, int argc, char *argv[])
     action.sa_flags = SA_RESTART;
     sigaction (SIGINT, &action, NULL);
 
-    argc--; argv++; /* skip subcommand argument */
+    notmuch_opt_desc_t options[] = {
+       { NOTMUCH_OPT_BOOLEAN, &batch, "batch", 0, 0 },
+       { NOTMUCH_OPT_STRING, &input_file_name, "input", 'i', 0 },
+       { 0, 0, 0, 0, 0 }
+    };
 
-    /* Array of tagging operations (add or remove), terminated with an
-     * empty element. */
-    tag_ops = talloc_array (ctx, tag_operation_t, argc + 1);
-    if (tag_ops == NULL) {
-       fprintf (stderr, "Out of memory.\n");
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
        return 1;
-    }
 
-    for (i = 0; i < argc; i++) {
-       if (strcmp (argv[i], "--") == 0) {
-           i++;
-           break;
+    if (input_file_name) {
+       batch = TRUE;
+       input = fopen (input_file_name, "r");
+       if (input == NULL) {
+           fprintf (stderr, "Error opening %s for reading: %s\n",
+                    input_file_name, strerror (errno));
+           return 1;
        }
-       if (argv[i][0] == '+' || argv[i][0] == '-') {
-           tag_ops[tag_ops_count].tag = argv[i] + 1;
-           tag_ops[tag_ops_count].remove = (argv[i][0] == '-');
-           tag_ops_count++;
-       } else {
-           break;
-       }
-    }
-
-    tag_ops[tag_ops_count].tag = NULL;
-
-    if (tag_ops_count == 0) {
-       fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
-       return 1;
     }
 
-    query_string = query_string_from_args (ctx, argc - i, &argv[i]);
+    if (batch) {
+       if (opt_index != argc) {
+           fprintf (stderr, "Can't specify both cmdline and stdin!\n");
+           return 1;
+       }
+    } else {
+       tag_ops = tag_op_list_create (ctx);
+       if (tag_ops == NULL) {
+           fprintf (stderr, "Out of memory.\n");
+           return 1;
+       }
 
-    if (*query_string == '\0') {
-       fprintf (stderr, "Error: notmuch tag requires at least one search term.\n");
-       return 1;
+       if (parse_tag_command_line (ctx, argc - opt_index, argv + opt_index,
+                                   &query_string, tag_ops))
+           return 1;
     }
 
     config = notmuch_config_open (ctx, NULL, NULL);
@@ -233,11 +244,18 @@ notmuch_tag_command (void *ctx, int argc, char *argv[])
                               NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
        return 1;
 
-    synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
+    if (notmuch_config_get_maildir_synchronize_flags (config))
+       tag_flags |= TAG_FLAG_MAILDIR_SYNC;
 
-    ret = tag_query (ctx, notmuch, query_string, tag_ops, synchronize_flags);
+    if (batch)
+       ret = tag_file (ctx, notmuch, tag_flags, input);
+    else
+       ret = tag_query (ctx, notmuch, query_string, tag_ops, tag_flags);
 
     notmuch_database_destroy (notmuch);
 
-    return ret;
+    if (input != stdin)
+       fclose (input);
+
+    return ret || interrupted;
 }
index 477a09cf05dd1bd803e8d68bfadf0e3c82660a65..4fc0973d3715ddd6a967d0c16f8f188b7f951c51 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -82,6 +82,8 @@ static command_t commands[] = {
       "This message, or more detailed help for the named command." }
 };
 
+int notmuch_format_version;
+
 static void
 usage (FILE *out)
 {
@@ -109,6 +111,33 @@ usage (FILE *out)
     "and \"notmuch help search-terms\" for the common search-terms syntax.\n\n");
 }
 
+void
+notmuch_exit_if_unsupported_format (void)
+{
+    if (notmuch_format_version > NOTMUCH_FORMAT_CUR) {
+       fprintf (stderr, "\
+A caller requested output format version %d, but the installed notmuch\n\
+CLI only supports up to format version %d.  You may need to upgrade your\n\
+notmuch CLI.\n",
+                notmuch_format_version, NOTMUCH_FORMAT_CUR);
+       exit (NOTMUCH_EXIT_FORMAT_TOO_NEW);
+    } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN) {
+       fprintf (stderr, "\
+A caller requested output format version %d, which is no longer supported\n\
+by the notmuch CLI (it requires at least version %d).  You may need to\n\
+upgrade your notmuch front-end.\n",
+                notmuch_format_version, NOTMUCH_FORMAT_MIN);
+       exit (NOTMUCH_EXIT_FORMAT_TOO_OLD);
+    } else if (notmuch_format_version != NOTMUCH_FORMAT_CUR) {
+       /* Warn about old version requests so compatibility issues are
+        * less likely when we drop support for a deprecated format
+        * versions. */
+       fprintf (stderr, "\
+A caller requested deprecated output format version %d, which may not\n\
+be supported in the future.\n", notmuch_format_version);
+    }
+}
+
 static void
 exec_man (const char *page)
 {
@@ -242,13 +271,16 @@ main (int argc, char *argv[])
     g_mime_init (0);
     g_type_init ();
 
+    /* Globally default to the current output format version. */
+    notmuch_format_version = NOTMUCH_FORMAT_CUR;
+
     if (argc == 1)
        return notmuch (local);
 
-    if (STRNCMP_LITERAL (argv[1], "--help") == 0)
+    if (strcmp (argv[1], "--help") == 0)
        return notmuch_help_command (NULL, argc - 1, &argv[1]);
 
-    if (STRNCMP_LITERAL (argv[1], "--version") == 0) {
+    if (strcmp (argv[1], "--version") == 0) {
        printf ("notmuch " STRINGIFY(NOTMUCH_VERSION) "\n");
        return 0;
     }
@@ -290,8 +322,28 @@ main (int argc, char *argv[])
     for (i = 0; i < ARRAY_SIZE (commands); i++) {
        command = &commands[i];
 
-       if (strcmp (argv[1], command->name) == 0)
-           return (command->function) (local, argc - 1, &argv[1]);
+       if (strcmp (argv[1], command->name) == 0) {
+           int ret;
+           char *talloc_report;
+
+           ret = (command->function)(local, argc - 1, &argv[1]);
+
+           /* in the future support for this environment variable may
+            * be supplemented or replaced by command line arguments
+            * --leak-report and/or --leak-report-full */
+
+           talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
+
+           /* this relies on the previous call to
+            * talloc_enable_null_tracking */
+
+           if (talloc_report && strcmp (talloc_report, "") != 0) {
+               FILE *report = fopen (talloc_report, "w");
+               talloc_report_full (NULL, report);
+           }
+
+           return ret;
+       }
     }
 
     fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
diff --git a/parse-time-string/Makefile b/parse-time-string/Makefile
new file mode 100644 (file)
index 0000000..fa25832
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/parse-time-string/Makefile.local b/parse-time-string/Makefile.local
new file mode 100644 (file)
index 0000000..53534f3
--- /dev/null
@@ -0,0 +1,12 @@
+dir := parse-time-string
+extra_cflags += -I$(srcdir)/$(dir)
+
+libparse-time-string_c_srcs := $(dir)/parse-time-string.c
+
+libparse-time-string_modules := $(libparse-time-string_c_srcs:.c=.o)
+
+$(dir)/libparse-time-string.a: $(libparse-time-string_modules)
+       $(call quiet,AR) rcs $@ $^
+
+SRCS := $(SRCS) $(libparse-time-string_c_srcs)
+CLEAN := $(CLEAN) $(libparse-time-string_modules) $(dir)/libparse-time-string.a
diff --git a/parse-time-string/README b/parse-time-string/README
new file mode 100644 (file)
index 0000000..300ff1f
--- /dev/null
@@ -0,0 +1,9 @@
+PARSE TIME STRING
+=================
+
+parse_time_string() is a date/time parser originally written for
+notmuch by Jani Nikula <jani@nikula.org>. However, there is nothing
+notmuch specific in it, and it should be kept reusable for other
+projects, and ready to be packaged on its own as needed. Please do not
+add dependencies on or references to anything notmuch specific. The
+parser should only depend on the C library.
diff --git a/parse-time-string/parse-time-string.c b/parse-time-string/parse-time-string.c
new file mode 100644 (file)
index 0000000..584067d
--- /dev/null
@@ -0,0 +1,1503 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include "parse-time-string.h"
+
+/*
+ * IMPLEMENTATION DETAILS
+ *
+ * At a high level, the parsing is done in two phases: 1) actual
+ * parsing of the input string and storing the parsed data into
+ * 'struct state', and 2) processing of the data in 'struct state'
+ * according to current time (or provided reference time) and
+ * rounding. This is evident in the main entry point function
+ * parse_time_string().
+ *
+ * 1) The parsing phase - parse_input()
+ *
+ * Parsing is greedy and happens from left to right. The parsing is as
+ * unambiguous as possible; only unambiguous date/time formats are
+ * accepted. Redundant or contradictory absolute date/time in the
+ * input (e.g. date specified multiple times/ways) is not
+ * accepted. Relative date/time on the other hand just accumulates if
+ * present multiple times (e.g. "5 days 5 days" just turns into 10
+ * days).
+ *
+ * Parsing decisions are made on the input format, not value. For
+ * example, "20/5/2005" fails because the recognized format here is
+ * MM/D/YYYY, even though the values would suggest DD/M/YYYY.
+ *
+ * Parsing is mostly stateless in the sense that parsing decisions are
+ * not made based on the values of previously parsed data, or whether
+ * certain data is present in the first place. (There are a few
+ * exceptions to the latter part, though, such as parsing of time zone
+ * that would otherwise look like plain time.)
+ *
+ * When the parser encounters a number that is not greedily parsed as
+ * part of a format, the interpretation is postponed until the next
+ * token is parsed. The parser for the next token may consume the
+ * previously postponed number. For example, when parsing "20 May" the
+ * meaning of "20" is not known until "May" is parsed. If the parser
+ * for the next token does not consume the postponed number, the
+ * number is handled as a "lone" number before parser for the next
+ * token finishes.
+ *
+ * 2) The processing phase - create_output()
+ *
+ * Once the parser in phase 1 has finished, 'struct state' contains
+ * all the information from the input string, and it's no longer
+ * needed. Since the parser does not even handle the concept of "now",
+ * the processing initializes the fields referring to the current
+ * date/time.
+ *
+ * If requested, the result is rounded towards past or future. The
+ * idea behind rounding is to support parsing date/time ranges in an
+ * obvious way. For example, for a range defined as two dates (without
+ * time), one would typically want to have an inclusive range from the
+ * beginning of start date to the end of the end date. The caller
+ * would use rounding towards past in the start date, and towards
+ * future in the end date.
+ *
+ * The absolute date and time is shifted by the relative date and
+ * time, and time zone adjustments are made. Daylight saving time
+ * (DST) is specifically *not* handled at all.
+ *
+ * Finally, the result is stored to time_t.
+ */
+
+#define unused(x) x __attribute__ ((unused))
+
+/* XXX: Redefine these to add i18n support. The keyword table uses
+ * N_() to mark strings to be translated; they are accessed
+ * dynamically using _(). */
+#define _(s) (s)       /* i18n: define as gettext (s) */
+#define N_(s) (s)      /* i18n: define as gettext_noop (s) */
+
+#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))
+
+/*
+ * Field indices in the tm and set arrays of struct state.
+ *
+ * NOTE: There's some code that depends on the ordering of this enum.
+ */
+enum field {
+    /* Keep SEC...YEAR in this order. */
+    TM_ABS_SEC,                /* seconds */
+    TM_ABS_MIN,                /* minutes */
+    TM_ABS_HOUR,       /* hours */
+    TM_ABS_MDAY,       /* day of the month */
+    TM_ABS_MON,                /* month */
+    TM_ABS_YEAR,       /* year */
+
+    TM_WDAY,           /* day of the week. special: may be relative */
+    TM_ABS_ISDST,      /* daylight saving time */
+
+    TM_AMPM,           /* am vs. pm */
+    TM_TZ,             /* timezone in minutes */
+
+    /* Keep SEC...YEAR in this order. */
+    TM_REL_SEC,                /* seconds relative to absolute or reference time */
+    TM_REL_MIN,                /* minutes ... */
+    TM_REL_HOUR,       /* hours ... */
+    TM_REL_DAY,                /* days ... */
+    TM_REL_MON,                /* months ... */
+    TM_REL_YEAR,       /* years ... */
+    TM_REL_WEEK,       /* weeks ... */
+
+    TM_NONE,           /* not a field */
+
+    TM_SIZE = TM_NONE,
+    TM_FIRST_ABS = TM_ABS_SEC,
+    TM_FIRST_REL = TM_REL_SEC,
+};
+
+/* Values for the set array of struct state. */
+enum field_set {
+    FIELD_UNSET,       /* The field has not been touched by parser. */
+    FIELD_SET,         /* The field has been set by parser. */
+    FIELD_NOW,         /* The field will be set to reference time. */
+};
+
+static enum field
+next_abs_field (enum field field)
+{
+    /* NOTE: Depends on the enum ordering. */
+    return field < TM_ABS_YEAR ? field + 1 : TM_NONE;
+}
+
+static enum field
+abs_to_rel_field (enum field field)
+{
+    assert (field <= TM_ABS_YEAR);
+
+    /* NOTE: Depends on the enum ordering. */
+    return field + (TM_FIRST_REL - TM_FIRST_ABS);
+}
+
+/* Get the smallest acceptable value for field. */
+static int
+get_field_epoch_value (enum field field)
+{
+    if (field == TM_ABS_MDAY || field == TM_ABS_MON)
+       return 1;
+    else if (field == TM_ABS_YEAR)
+       return 1970;
+    else
+       return 0;
+}
+
+/* The parsing state. */
+struct state {
+    int tm[TM_SIZE];                   /* parsed date and time */
+    enum field_set set[TM_SIZE];       /* set status of tm */
+
+    enum field last_field;     /* Previously set field. */
+    char delim;
+
+    int postponed_length;      /* Number of digits in postponed value. */
+    int postponed_value;
+    char postponed_delim;      /* The delimiter preceding postponed number. */
+};
+
+/*
+ * Helpers for postponed numbers.
+ *
+ * postponed_length is the number of digits in postponed value. 0
+ * means there is no postponed number. -1 means there is a postponed
+ * number, but it comes from a keyword, and it doesn't have digits.
+ */
+static int
+get_postponed_length (struct state *state)
+{
+    return state->postponed_length;
+}
+
+/*
+ * Consume a previously postponed number. Return true if a number was
+ * in fact postponed, false otherwise. Store the postponed number's
+ * value in *v, length in the input string in *n (or -1 if the number
+ * was written out and parsed as a keyword), and the preceding
+ * delimiter to *d. If a number was not postponed, *v, *n and *d are
+ * unchanged.
+ */
+static bool
+consume_postponed_number (struct state *state, int *v, int *n, char *d)
+{
+    if (!state->postponed_length)
+       return false;
+
+    if (n)
+       *n = state->postponed_length;
+
+    if (v)
+       *v = state->postponed_value;
+
+    if (d)
+       *d = state->postponed_delim;
+
+    state->postponed_length = 0;
+    state->postponed_value = 0;
+    state->postponed_delim = 0;
+
+    return true;
+}
+
+static int parse_postponed_number (struct state *state, enum field next_field);
+
+/*
+ * Postpone a number to be handled later. If one exists already,
+ * handle it first. n may be -1 to indicate a keyword that has no
+ * number length.
+ */
+static int
+set_postponed_number (struct state *state, int v, int n)
+{
+    int r;
+    char d = state->delim;
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, TM_NONE);
+    if (r)
+       return r;
+
+    state->postponed_length = n;
+    state->postponed_value = v;
+    state->postponed_delim = d;
+
+    return 0;
+}
+
+static void
+set_delim (struct state *state, char delim)
+{
+    state->delim = delim;
+}
+
+static void
+unset_delim (struct state *state)
+{
+    state->delim = 0;
+}
+
+/*
+ * Field set/get/mod helpers.
+ */
+
+/* Return true if field has been set. */
+static bool
+is_field_set (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    return state->set[field] != FIELD_UNSET;
+}
+
+static void
+unset_field (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    state->set[field] = FIELD_UNSET;
+    state->tm[field] = 0;
+}
+
+/*
+ * Set field to value. A field can only be set once to ensure the
+ * input does not contain redundant and potentially conflicting data.
+ */
+static int
+set_field (struct state *state, enum field field, int value)
+{
+    int r;
+
+    /* Fields can only be set once. */
+    if (is_field_set (state, field))
+       return -PARSE_TIME_ERR_ALREADYSET;
+
+    state->set[field] = FIELD_SET;
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, field);
+    if (r)
+       return r;
+
+    unset_delim (state);
+
+    state->tm[field] = value;
+    state->last_field = field;
+
+    return 0;
+}
+
+/*
+ * Mark n fields in fields to be set to the reference date/time in the
+ * specified time zone, or local timezone if not specified. The fields
+ * will be initialized after parsing is complete and timezone is
+ * known.
+ */
+static int
+set_fields_to_now (struct state *state, enum field *fields, size_t n)
+{
+    size_t i;
+    int r;
+
+    for (i = 0; i < n; i++) {
+       r = set_field (state, fields[i], 0);
+       if (r)
+           return r;
+       state->set[fields[i]] = FIELD_NOW;
+    }
+
+    return 0;
+}
+
+/* Modify field by adding value to it. To be used on relative fields,
+ * which can be modified multiple times (to accumulate). */
+static int
+add_to_field (struct state *state, enum field field, int value)
+{
+    int r;
+
+    assert (field < ARRAY_SIZE (state->tm));
+
+    state->set[field] = FIELD_SET;
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, field);
+    if (r)
+       return r;
+
+    unset_delim (state);
+
+    state->tm[field] += value;
+    state->last_field = field;
+
+    return 0;
+}
+
+/*
+ * Get field value. Make sure the field is set before query. It's most
+ * likely an error to call this while parsing (for example fields set
+ * as FIELD_NOW will only be set to some value after parsing).
+ */
+static int
+get_field (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    return state->tm[field];
+}
+
+/*
+ * Validity checkers.
+ */
+static bool is_valid_12hour (int h)
+{
+    return h >= 1 && h <= 12;
+}
+
+static bool is_valid_time (int h, int m, int s)
+{
+    /* Allow 24:00:00 to denote end of day. */
+    if (h == 24 && m == 0 && s == 0)
+       return true;
+
+    return h >= 0 && h <= 23 && m >= 0 && m <= 59 && s >= 0 && s <= 59;
+}
+
+static bool is_valid_mday (int mday)
+{
+    return mday >= 1 && mday <= 31;
+}
+
+static bool is_valid_mon (int mon)
+{
+    return mon >= 1 && mon <= 12;
+}
+
+static bool is_valid_year (int year)
+{
+    return year >= 1970;
+}
+
+static bool is_valid_date (int year, int mon, int mday)
+{
+    return is_valid_year (year) && is_valid_mon (mon) && is_valid_mday (mday);
+}
+
+/* Unset indicator for time and date set helpers. */
+#define UNSET -1
+
+/* Time set helper. No input checking. Use UNSET (-1) to leave unset. */
+static int
+set_abs_time (struct state *state, int hour, int min, int sec)
+{
+    int r;
+
+    if (hour != UNSET) {
+       if ((r = set_field (state, TM_ABS_HOUR, hour)))
+           return r;
+    }
+
+    if (min != UNSET) {
+       if ((r = set_field (state, TM_ABS_MIN, min)))
+           return r;
+    }
+
+    if (sec != UNSET) {
+       if ((r = set_field (state, TM_ABS_SEC, sec)))
+           return r;
+    }
+
+    return 0;
+}
+
+/* Date set helper. No input checking. Use UNSET (-1) to leave unset. */
+static int
+set_abs_date (struct state *state, int year, int mon, int mday)
+{
+    int r;
+
+    if (year != UNSET) {
+       if ((r = set_field (state, TM_ABS_YEAR, year)))
+           return r;
+    }
+
+    if (mon != UNSET) {
+       if ((r = set_field (state, TM_ABS_MON, mon)))
+           return r;
+    }
+
+    if (mday != UNSET) {
+       if ((r = set_field (state, TM_ABS_MDAY, mday)))
+           return r;
+    }
+
+    return 0;
+}
+
+/*
+ * Keyword parsing and handling.
+ */
+struct keyword;
+typedef int (*setter_t)(struct state *state, struct keyword *kw);
+
+struct keyword {
+    const char *name;  /* keyword */
+    enum field field;  /* field to set, or FIELD_NONE if N/A */
+    int value;         /* value to set, or 0 if N/A */
+    setter_t set;      /* function to use for setting, if non-NULL */
+};
+
+/*
+ * Setter callback functions for keywords.
+ */
+static int
+kw_set_rel (struct state *state, struct keyword *kw)
+{
+    int multiplier = 1;
+
+    /* Get a previously set multiplier, if any. */
+    consume_postponed_number (state, &multiplier, NULL, NULL);
+
+    /* Accumulate relative field values. */
+    return add_to_field (state, kw->field, multiplier * kw->value);
+}
+
+static int
+kw_set_number (struct state *state, struct keyword *kw)
+{
+    /* -1 = no length, from keyword. */
+    return set_postponed_number (state, kw->value, -1);
+}
+
+static int
+kw_set_month (struct state *state, struct keyword *kw)
+{
+    int n = get_postponed_length (state);
+
+    /* Consume postponed number if it could be mday. This handles "20
+     * January". */
+    if (n == 1 || n == 2) {
+       int r, v;
+
+       consume_postponed_number (state, &v, NULL, NULL);
+
+       if (!is_valid_mday (v))
+           return -PARSE_TIME_ERR_INVALIDDATE;
+
+       r = set_field (state, TM_ABS_MDAY, v);
+       if (r)
+           return r;
+    }
+
+    return set_field (state, kw->field, kw->value);
+}
+
+static int
+kw_set_ampm (struct state *state, struct keyword *kw)
+{
+    int n = get_postponed_length (state);
+
+    /* Consume postponed number if it could be hour. This handles
+     * "5pm". */
+    if (n == 1 || n == 2) {
+       int r, v;
+
+       consume_postponed_number (state, &v, NULL, NULL);
+
+       if (!is_valid_12hour (v))
+           return -PARSE_TIME_ERR_INVALIDTIME;
+
+       r = set_abs_time (state, v, 0, 0);
+       if (r)
+           return r;
+    }
+
+    return set_field (state, kw->field, kw->value);
+}
+
+static int
+kw_set_timeofday (struct state *state, struct keyword *kw)
+{
+    return set_abs_time (state, kw->value, 0, 0);
+}
+
+static int
+kw_set_today (struct state *state, unused (struct keyword *kw))
+{
+    enum field fields[] = { TM_ABS_YEAR, TM_ABS_MON, TM_ABS_MDAY };
+
+    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));
+}
+
+static int
+kw_set_now (struct state *state, unused (struct keyword *kw))
+{
+    enum field fields[] = { TM_ABS_HOUR, TM_ABS_MIN, TM_ABS_SEC };
+
+    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));
+}
+
+static int
+kw_set_ordinal (struct state *state, struct keyword *kw)
+{
+    int n, v;
+
+    /* Require a postponed number. */
+    if (!consume_postponed_number (state, &v, &n, NULL))
+       return -PARSE_TIME_ERR_DATEFORMAT;
+
+    /* Ordinals are mday. */
+    if (n != 1 && n != 2)
+       return -PARSE_TIME_ERR_DATEFORMAT;
+
+    /* Be strict about st, nd, rd, and lax about th. */
+    if (strcasecmp (kw->name, "st") == 0 && v != 1 && v != 21 && v != 31)
+       return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "nd") == 0 && v != 2 && v != 22)
+       return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "rd") == 0 && v != 3 && v != 23)
+       return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "th") == 0 && !is_valid_mday (v))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    return set_field (state, TM_ABS_MDAY, v);
+}
+
+static int
+kw_ignore (unused (struct state *state), unused (struct keyword *kw))
+{
+    return 0;
+}
+
+/*
+ * Accepted keywords.
+ *
+ * A keyword may optionally contain a '|' to indicate the minimum
+ * match length. Without one, full match is required. It's advisable
+ * to keep the minimum match parts unique across all keywords. If
+ * they're not, the first match wins.
+ *
+ * If keyword begins with '*', then the matching will be case
+ * sensitive. Otherwise the matching is case insensitive.
+ *
+ * If .set is NULL, the field specified by .field will be set to
+ * .value.
+ *
+ * Note: Observe how "m" and "mi" match minutes, "M" and "mo" and
+ * "mont" match months, but "mon" matches Monday.
+ */
+static struct keyword keywords[] = {
+    /* Weekdays. */
+    { N_("sun|day"),   TM_WDAY,        0,      NULL },
+    { N_("mon|day"),   TM_WDAY,        1,      NULL },
+    { N_("tue|sday"),  TM_WDAY,        2,      NULL },
+    { N_("wed|nesday"),        TM_WDAY,        3,      NULL },
+    { N_("thu|rsday"), TM_WDAY,        4,      NULL },
+    { N_("fri|day"),   TM_WDAY,        5,      NULL },
+    { N_("sat|urday"), TM_WDAY,        6,      NULL },
+
+    /* Months. */
+    { N_("jan|uary"),  TM_ABS_MON,     1,      kw_set_month },
+    { N_("feb|ruary"), TM_ABS_MON,     2,      kw_set_month },
+    { N_("mar|ch"),    TM_ABS_MON,     3,      kw_set_month },
+    { N_("apr|il"),    TM_ABS_MON,     4,      kw_set_month },
+    { N_("may"),       TM_ABS_MON,     5,      kw_set_month },
+    { N_("jun|e"),     TM_ABS_MON,     6,      kw_set_month },
+    { N_("jul|y"),     TM_ABS_MON,     7,      kw_set_month },
+    { N_("aug|ust"),   TM_ABS_MON,     8,      kw_set_month },
+    { N_("sep|tember"),        TM_ABS_MON,     9,      kw_set_month },
+    { N_("oct|ober"),  TM_ABS_MON,     10,     kw_set_month },
+    { N_("nov|ember"), TM_ABS_MON,     11,     kw_set_month },
+    { N_("dec|ember"), TM_ABS_MON,     12,     kw_set_month },
+
+    /* Durations. */
+    { N_("y|ears"),    TM_REL_YEAR,    1,      kw_set_rel },
+    { N_("mo|nths"),   TM_REL_MON,     1,      kw_set_rel },
+    { N_("*M"),                TM_REL_MON,     1,      kw_set_rel },
+    { N_("w|eeks"),    TM_REL_WEEK,    1,      kw_set_rel },
+    { N_("d|ays"),     TM_REL_DAY,     1,      kw_set_rel },
+    { N_("h|ours"),    TM_REL_HOUR,    1,      kw_set_rel },
+    { N_("hr|s"),      TM_REL_HOUR,    1,      kw_set_rel },
+    { N_("mi|nutes"),  TM_REL_MIN,     1,      kw_set_rel },
+    { N_("mins"),      TM_REL_MIN,     1,      kw_set_rel },
+    { N_("*m"),                TM_REL_MIN,     1,      kw_set_rel },
+    { N_("s|econds"),  TM_REL_SEC,     1,      kw_set_rel },
+    { N_("secs"),      TM_REL_SEC,     1,      kw_set_rel },
+
+    /* Numbers. */
+    { N_("one"),       TM_NONE,        1,      kw_set_number },
+    { N_("two"),       TM_NONE,        2,      kw_set_number },
+    { N_("three"),     TM_NONE,        3,      kw_set_number },
+    { N_("four"),      TM_NONE,        4,      kw_set_number },
+    { N_("five"),      TM_NONE,        5,      kw_set_number },
+    { N_("six"),       TM_NONE,        6,      kw_set_number },
+    { N_("seven"),     TM_NONE,        7,      kw_set_number },
+    { N_("eight"),     TM_NONE,        8,      kw_set_number },
+    { N_("nine"),      TM_NONE,        9,      kw_set_number },
+    { N_("ten"),       TM_NONE,        10,     kw_set_number },
+    { N_("dozen"),     TM_NONE,        12,     kw_set_number },
+    { N_("hundred"),   TM_NONE,        100,    kw_set_number },
+
+    /* Special number forms. */
+    { N_("this"),      TM_NONE,        0,      kw_set_number },
+    { N_("last"),      TM_NONE,        1,      kw_set_number },
+
+    /* Other special keywords. */
+    { N_("yesterday"), TM_REL_DAY,     1,      kw_set_rel },
+    { N_("today"),     TM_NONE,        0,      kw_set_today },
+    { N_("now"),       TM_NONE,        0,      kw_set_now },
+    { N_("noon"),      TM_NONE,        12,     kw_set_timeofday },
+    { N_("midnight"),  TM_NONE,        0,      kw_set_timeofday },
+    { N_("am"),                TM_AMPM,        0,      kw_set_ampm },
+    { N_("a.m."),      TM_AMPM,        0,      kw_set_ampm },
+    { N_("pm"),                TM_AMPM,        1,      kw_set_ampm },
+    { N_("p.m."),      TM_AMPM,        1,      kw_set_ampm },
+    { N_("st"),                TM_NONE,        0,      kw_set_ordinal },
+    { N_("nd"),                TM_NONE,        0,      kw_set_ordinal },
+    { N_("rd"),                TM_NONE,        0,      kw_set_ordinal },
+    { N_("th"),                TM_NONE,        0,      kw_set_ordinal },
+    { N_("ago"),               TM_NONE,        0,      kw_ignore },
+
+    /* Timezone codes: offset in minutes. XXX: Add more codes. */
+    { N_("pst"),       TM_TZ,          -8*60,  NULL },
+    { N_("mst"),       TM_TZ,          -7*60,  NULL },
+    { N_("cst"),       TM_TZ,          -6*60,  NULL },
+    { N_("est"),       TM_TZ,          -5*60,  NULL },
+    { N_("ast"),       TM_TZ,          -4*60,  NULL },
+    { N_("nst"),       TM_TZ,          -(3*60+30),     NULL },
+
+    { N_("gmt"),       TM_TZ,          0,      NULL },
+    { N_("utc"),       TM_TZ,          0,      NULL },
+
+    { N_("wet"),       TM_TZ,          0,      NULL },
+    { N_("cet"),       TM_TZ,          1*60,   NULL },
+    { N_("eet"),       TM_TZ,          2*60,   NULL },
+    { N_("fet"),       TM_TZ,          3*60,   NULL },
+
+    { N_("wat"),       TM_TZ,          1*60,   NULL },
+    { N_("cat"),       TM_TZ,          2*60,   NULL },
+    { N_("eat"),       TM_TZ,          3*60,   NULL },
+};
+
+/*
+ * Compare strings str and keyword. Return the number of matching
+ * chars on match, 0 for no match.
+ *
+ * All of the alphabetic characters (isalpha) in str up to the first
+ * non-alpha character (or end of string) must match the
+ * keyword. Consequently, the value returned on match is the number of
+ * consecutive alphabetic characters in str.
+ *
+ * Abbreviated match is accepted if the keyword contains a '|'
+ * character, and str matches keyword up to that character. Any alpha
+ * characters after that in str must still match the keyword following
+ * the '|' character. If no '|' is present, all of keyword must match.
+ *
+ * Excessive, consecutive, and misplaced (at the beginning or end) '|'
+ * characters in keyword are handled gracefully. Only the first one
+ * matters.
+ *
+ * If match_case is true, the matching is case sensitive.
+ */
+static size_t
+match_keyword (const char *str, const char *keyword, bool match_case)
+{
+    const char *s = str;
+    bool prefix_matched = false;
+
+    for (;;) {
+       while (*keyword == '|') {
+           prefix_matched = true;
+           keyword++;
+       }
+
+       if (!*s || !isalpha ((unsigned char) *s) || !*keyword)
+           break;
+
+       if (match_case) {
+           if (*s != *keyword)
+               return 0;
+       } else {
+           if (tolower ((unsigned char) *s) !=
+               tolower ((unsigned char) *keyword))
+               return 0;
+       }
+       s++;
+       keyword++;
+    }
+
+    /* did not match all of the keyword in input string */
+    if (*s && isalpha ((unsigned char) *s))
+       return 0;
+
+    /* did not match enough of keyword */
+    if (*keyword && !prefix_matched)
+       return 0;
+
+    return s - str;
+}
+
+/*
+ * Parse a keyword. Return < 0 on error, number of parsed chars on
+ * success.
+ */
+static ssize_t
+parse_keyword (struct state *state, const char *s)
+{
+    unsigned int i;
+    size_t n = 0;
+    struct keyword *kw = NULL;
+    int r;
+
+    for (i = 0; i < ARRAY_SIZE (keywords); i++) {
+       const char *keyword = _(keywords[i].name);
+       bool mcase = false;
+
+       /* Match case if keyword begins with '*'. */
+       if (*keyword == '*') {
+           mcase = true;
+           keyword++;
+       }
+
+       n = match_keyword (s, keyword, mcase);
+       if (n) {
+           kw = &keywords[i];
+           break;
+       }
+    }
+
+    if (!kw)
+       return -PARSE_TIME_ERR_KEYWORD;
+
+    if (kw->set)
+       r = kw->set (state, kw);
+    else
+       r = set_field (state, kw->field, kw->value);
+
+    if (r < 0)
+       return r;
+
+    return n;
+}
+
+/*
+ * Non-keyword parsers and their helpers.
+ */
+
+static int
+set_user_tz (struct state *state, char sign, int hour, int min)
+{
+    int tz = hour * 60 + min;
+
+    assert (sign == '+' || sign == '-');
+
+    if (hour < 0 || hour > 14 || min < 0 || min > 59 || min % 15)
+       return -PARSE_TIME_ERR_INVALIDTIME;
+
+    if (sign == '-')
+       tz = -tz;
+
+    return set_field (state, TM_TZ, tz);
+}
+
+/*
+ * Parse a previously postponed number if one exists. Independent
+ * parsing of a postponed number when it wasn't consumed during
+ * parsing of the following token.
+ */
+static int
+parse_postponed_number (struct state *state, unused (enum field next_field))
+{
+    int v, n;
+    char d;
+
+    /* Bail out if there's no postponed number. */
+    if (!consume_postponed_number (state, &v, &n, &d))
+       return 0;
+
+    if (n == 1 || n == 2) {
+       /* Notable exception: Previous field affects parsing. This
+        * handles "January 20". */
+       if (state->last_field == TM_ABS_MON) {
+           /* D[D] */
+           if (!is_valid_mday (v))
+               return -PARSE_TIME_ERR_INVALIDDATE;
+
+           return set_field (state, TM_ABS_MDAY, v);
+       } else if (n == 2) {
+           /* XXX: Only allow if last field is hour, min, or sec? */
+           if (d == '+' || d == '-') {
+               /* +/-HH */
+               return set_user_tz (state, d, v, 0);
+           }
+       }
+    } else if (n == 4) {
+       /* Notable exception: Value affects parsing. Time zones are
+        * always at most 1400 and we don't understand years before
+        * 1970. */
+       if (!is_valid_year (v)) {
+           if (d == '+' || d == '-') {
+               /* +/-HHMM */
+               return set_user_tz (state, d, v / 100, v % 100);
+           }
+       } else {
+           /* YYYY */
+           return set_field (state, TM_ABS_YEAR, v);
+       }
+    } else if (n == 6) {
+       /* HHMMSS */
+       int hour = v / 10000;
+       int min = (v / 100) % 100;
+       int sec = v % 100;
+
+       if (!is_valid_time (hour, min, sec))
+           return -PARSE_TIME_ERR_INVALIDTIME;
+
+       return set_abs_time (state, hour, min, sec);
+    } else if (n == 8) {
+       /* YYYYMMDD */
+       int year = v / 10000;
+       int mon = (v / 100) % 100;
+       int mday = v % 100;
+
+       if (!is_valid_date (year, mon, mday))
+           return -PARSE_TIME_ERR_INVALIDDATE;
+
+       return set_abs_date (state, year, mon, mday);
+    }
+
+    return -PARSE_TIME_ERR_FORMAT;
+}
+
+static int tm_get_field (const struct tm *tm, enum field field);
+
+static int
+set_timestamp (struct state *state, time_t t)
+{
+    struct tm tm;
+    enum field f;
+    int r;
+
+    if (gmtime_r (&t, &tm) == NULL)
+       return -PARSE_TIME_ERR_LIB;
+
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) {
+       r = set_field (state, f, tm_get_field (&tm, f));
+       if (r)
+           return r;
+    }
+
+    r = set_field (state, TM_TZ, 0);
+    if (r)
+       return r;
+
+    /* XXX: Prevent TM_AMPM with timestamp, e.g. "@123456 pm" */
+
+    return 0;
+}
+
+/* Parse a single number. Typically postpone parsing until later. */
+static int
+parse_single_number (struct state *state, unsigned long v,
+                    unsigned long n)
+{
+    assert (n);
+
+    if (state->delim == '@')
+       return set_timestamp (state, (time_t) v);
+
+    if (v > INT_MAX)
+       return -PARSE_TIME_ERR_FORMAT;
+
+    return set_postponed_number (state, v, n);
+}
+
+static bool
+is_time_sep (char c)
+{
+    return c == ':';
+}
+
+static bool
+is_date_sep (char c)
+{
+    return c == '/' || c == '-' || c == '.';
+}
+
+static bool
+is_sep (char c)
+{
+    return is_time_sep (c) || is_date_sep (c);
+}
+
+/* Two-digit year: 00...69 is 2000s, 70...99 1900s, if n == 0 keep
+ * unset. */
+static int
+expand_year (unsigned long year, size_t n)
+{
+    if (n == 2) {
+       return (year < 70 ? 2000 : 1900) + year;
+    } else if (n == 4) {
+       return year;
+    } else {
+       return UNSET;
+    }
+}
+
+/* Parse a date number triplet. */
+static int
+parse_date (struct state *state, char sep,
+           unsigned long v1, unsigned long v2, unsigned long v3,
+           size_t n1, size_t n2, size_t n3)
+{
+    int year = UNSET, mon = UNSET, mday = UNSET;
+
+    assert (is_date_sep (sep));
+
+    switch (sep) {
+    case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */
+       if (n1 != 1 && n1 != 2)
+           return -PARSE_TIME_ERR_DATEFORMAT;
+
+       if ((n2 == 1 || n2 == 2) && (n3 == 0 || n3 == 2 || n3 == 4)) {
+           /* M[M]/D[D][/YY[YY]] */
+           year = expand_year (v3, n3);
+           mon = v1;
+           mday = v2;
+       } else if (n2 == 4 && n3 == 0) {
+           /* M[M]/YYYY */
+           year = v2;
+           mon = v1;
+       } else {
+           return -PARSE_TIME_ERR_DATEFORMAT;
+       }
+       break;
+
+    case '-': /* Date: YYYY-MM[-DD] or DD-MM[-YY[YY]] or MM-YYYY */
+       if (n1 == 4 && n2 == 2 && (n3 == 0 || n3 == 2)) {
+           /* YYYY-MM[-DD] */
+           year = v1;
+           mon = v2;
+           if (n3)
+               mday = v3;
+       } else if (n1 == 2 && n2 == 2 && (n3 == 0 || n3 == 2 || n3 == 4)) {
+           /* DD-MM[-YY[YY]] */
+           year = expand_year (v3, n3);
+           mon = v2;
+           mday = v1;
+       } else if (n1 == 2 && n2 == 4 && n3 == 0) {
+           /* MM-YYYY */
+           year = v2;
+           mon = v1;
+       } else {
+           return -PARSE_TIME_ERR_DATEFORMAT;
+       }
+       break;
+
+    case '.': /* Date: D[D].M[M][.[YY[YY]]] */
+       if ((n1 != 1 && n1 != 2) || (n2 != 1 && n2 != 2) ||
+           (n3 != 0 && n3 != 2 && n3 != 4))
+           return -PARSE_TIME_ERR_DATEFORMAT;
+
+       year = expand_year (v3, n3);
+       mon = v2;
+       mday = v1;
+       break;
+    }
+
+    if (year != UNSET && !is_valid_year (year))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    if (mon != UNSET && !is_valid_mon (mon))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    if (mday != UNSET && !is_valid_mday (mday))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    return set_abs_date (state, year, mon, mday);
+}
+
+/* Parse a time number triplet. */
+static int
+parse_time (struct state *state, char sep,
+           unsigned long v1, unsigned long v2, unsigned long v3,
+           size_t n1, size_t n2, size_t n3)
+{
+    assert (is_time_sep (sep));
+
+    if ((n1 != 1 && n1 != 2) || n2 != 2 || (n3 != 0 && n3 != 2))
+       return -PARSE_TIME_ERR_TIMEFORMAT;
+
+    /*
+     * Notable exception: Previously set fields affect
+     * parsing. Interpret (+|-)HH:MM as time zone only if hour and
+     * minute have been set.
+     *
+     * XXX: This could be fixed by restricting the delimiters
+     * preceding time. For '+' it would be justified, but for '-' it
+     * might be inconvenient. However prefer to allow '-' as an
+     * insignificant delimiter preceding time for convenience, and
+     * handle '+' the same way for consistency between positive and
+     * negative time zones.
+     */
+    if (is_field_set (state, TM_ABS_HOUR) &&
+       is_field_set (state, TM_ABS_MIN) &&
+       n1 == 2 && n2 == 2 && n3 == 0 &&
+       (state->delim == '+' || state->delim == '-')) {
+       return set_user_tz (state, state->delim, v1, v2);
+    }
+
+    if (!is_valid_time (v1, v2, v3))
+       return -PARSE_TIME_ERR_INVALIDTIME;
+
+    return set_abs_time (state, v1, v2, n3 ? v3 : 0);
+}
+
+/* strtoul helper that assigns length. */
+static unsigned long
+strtoul_len (const char *s, const char **endp, size_t *len)
+{
+    unsigned long val = strtoul (s, (char **) endp, 10);
+
+    *len = *endp - s;
+    return val;
+}
+
+/*
+ * Parse a (group of) number(s). Return < 0 on error, number of parsed
+ * chars on success.
+ */
+static ssize_t
+parse_number (struct state *state, const char *s)
+{
+    int r;
+    unsigned long v1, v2, v3 = 0;
+    size_t n1, n2, n3 = 0;
+    const char *p = s;
+    char sep;
+
+    v1 = strtoul_len (p, &p, &n1);
+
+    if (!is_sep (*p) || !isdigit ((unsigned char) *(p + 1))) {
+       /* A single number. */
+       r = parse_single_number (state, v1, n1);
+       if (r)
+           return r;
+
+       return p - s;
+    }
+
+    sep = *p;
+    v2 = strtoul_len (p + 1, &p, &n2);
+
+    /* A group of two or three numbers? */
+    if (*p == sep && isdigit ((unsigned char) *(p + 1)))
+       v3 = strtoul_len (p + 1, &p, &n3);
+
+    if (is_time_sep (sep))
+       r = parse_time (state, sep, v1, v2, v3, n1, n2, n3);
+    else
+       r = parse_date (state, sep, v1, v2, v3, n1, n2, n3);
+
+    if (r)
+       return r;
+
+    return p - s;
+}
+
+/*
+ * Parse delimiter(s). Throw away all except the last one, which is
+ * stored for parsing the next non-delimiter. Return < 0 on error,
+ * number of parsed chars on success.
+ *
+ * XXX: We might want to be more strict here.
+ */
+static ssize_t
+parse_delim (struct state *state, const char *s)
+{
+    const char *p = s;
+
+    /*
+     * Skip non-alpha and non-digit, and store the last for further
+     * processing.
+     */
+    while (*p && !isalnum ((unsigned char) *p)) {
+       set_delim (state, *p);
+       p++;
+    }
+
+    return p - s;
+}
+
+/*
+ * Parse a date/time string. Return < 0 on error, number of parsed
+ * chars on success.
+ */
+static ssize_t
+parse_input (struct state *state, const char *s)
+{
+    const char *p = s;
+    ssize_t n;
+    int r;
+
+    while (*p) {
+       if (isalpha ((unsigned char) *p)) {
+           n = parse_keyword (state, p);
+       } else if (isdigit ((unsigned char) *p)) {
+           n = parse_number (state, p);
+       } else {
+           n = parse_delim (state, p);
+       }
+
+       if (n <= 0) {
+           if (n == 0)
+               n = -PARSE_TIME_ERR;
+
+           return n;
+       }
+
+       p += n;
+    }
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, TM_NONE);
+    if (r < 0)
+       return r;
+
+    return p - s;
+}
+
+/*
+ * Processing the parsed input.
+ */
+
+/*
+ * Initialize reference time to tm. Use time zone in state if
+ * specified, otherwise local time. Use now for reference time if
+ * non-NULL, otherwise current time.
+ */
+static int
+initialize_now (struct state *state, const time_t *ref, struct tm *tm)
+{
+    time_t t;
+
+    if (ref) {
+       t = *ref;
+    } else {
+       if (time (&t) == (time_t) -1)
+           return -PARSE_TIME_ERR_LIB;
+    }
+
+    if (is_field_set (state, TM_TZ)) {
+       /* Some other time zone. */
+
+       /* Adjust now according to the TZ. */
+       t += get_field (state, TM_TZ) * 60;
+
+       /* It's not gm, but this doesn't mess with the TZ. */
+       if (gmtime_r (&t, tm) == NULL)
+           return -PARSE_TIME_ERR_LIB;
+    } else {
+       /* Local time. */
+       if (localtime_r (&t, tm) == NULL)
+           return -PARSE_TIME_ERR_LIB;
+    }
+
+    return 0;
+}
+
+/*
+ * Normalize tm according to mktime(3); if structure members are
+ * outside their valid interval, they will be normalized (so that, for
+ * example, 40 October is changed into 9 November), and tm_wday and
+ * tm_yday are set to values determined from the contents of the other
+ * fields.
+ *
+ * Both mktime(3) and localtime_r(3) use local time, but they cancel
+ * each other out here, making this function agnostic to time zone.
+ */
+static int
+normalize_tm (struct tm *tm)
+{
+    time_t t = mktime (tm);
+
+    if (t == (time_t) -1)
+       return -PARSE_TIME_ERR_LIB;
+
+    if (!localtime_r (&t, tm))
+       return -PARSE_TIME_ERR_LIB;
+
+    return 0;
+}
+
+/* Get field out of a struct tm. */
+static int
+tm_get_field (const struct tm *tm, enum field field)
+{
+    switch (field) {
+    case TM_ABS_SEC:   return tm->tm_sec;
+    case TM_ABS_MIN:   return tm->tm_min;
+    case TM_ABS_HOUR:  return tm->tm_hour;
+    case TM_ABS_MDAY:  return tm->tm_mday;
+    case TM_ABS_MON:   return tm->tm_mon + 1; /* 0- to 1-based */
+    case TM_ABS_YEAR:  return 1900 + tm->tm_year;
+    case TM_WDAY:      return tm->tm_wday;
+    case TM_ABS_ISDST: return tm->tm_isdst;
+    default:
+       assert (false);
+       break;
+    }
+
+    return 0;
+}
+
+/* Modify hour according to am/pm setting. */
+static int
+fixup_ampm (struct state *state)
+{
+    int hour, hdiff = 0;
+
+    if (!is_field_set (state, TM_AMPM))
+       return 0;
+
+    if (!is_field_set (state, TM_ABS_HOUR))
+       return -PARSE_TIME_ERR_TIMEFORMAT;
+
+    hour = get_field (state, TM_ABS_HOUR);
+    if (!is_valid_12hour (hour))
+       return -PARSE_TIME_ERR_INVALIDTIME;
+
+    if (get_field (state, TM_AMPM)) {
+       /* 12pm is noon. */
+       if (hour != 12)
+           hdiff = 12;
+    } else {
+       /* 12am is midnight, beginning of day. */
+       if (hour == 12)
+           hdiff = -12;
+    }
+
+    add_to_field (state, TM_REL_HOUR, -hdiff);
+
+    return 0;
+}
+
+/* Combine absolute and relative fields, and round. */
+static int
+create_output (struct state *state, time_t *t_out, const time_t *ref,
+              int round)
+{
+    struct tm tm = { .tm_isdst = -1 };
+    struct tm now;
+    time_t t;
+    enum field f;
+    int r;
+    int week_round = PARSE_TIME_NO_ROUND;
+
+    r = initialize_now (state, ref, &now);
+    if (r)
+       return r;
+
+    /* Initialize fields flagged as "now" to reference time. */
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) {
+       if (state->set[f] == FIELD_NOW) {
+           state->tm[f] = tm_get_field (&now, f);
+           state->set[f] = FIELD_SET;
+       }
+    }
+
+    /*
+     * If WDAY is set but MDAY is not, we consider WDAY relative
+     *
+     * XXX: This fails on stuff like "two months monday" because two
+     * months ago wasn't the same day as today. Postpone until we know
+     * date?
+     */
+    if (is_field_set (state, TM_WDAY) &&
+       !is_field_set (state, TM_ABS_MDAY)) {
+       int wday = get_field (state, TM_WDAY);
+       int today = tm_get_field (&now, TM_WDAY);
+       int rel_days;
+
+       if (today > wday)
+           rel_days = today - wday;
+       else
+           rel_days = today + 7 - wday;
+
+       /* This also prevents special week rounding from happening. */
+       add_to_field (state, TM_REL_DAY, rel_days);
+
+       unset_field (state, TM_WDAY);
+    }
+
+    r = fixup_ampm (state);
+    if (r)
+       return r;
+
+    /*
+     * Iterate fields from most accurate to least accurate, and set
+     * unset fields according to requested rounding.
+     */
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) {
+       if (round != PARSE_TIME_NO_ROUND) {
+           enum field r = abs_to_rel_field (f);
+
+           if (is_field_set (state, f) || is_field_set (state, r)) {
+               if (round >= PARSE_TIME_ROUND_UP && f != TM_ABS_SEC) {
+                   /*
+                    * This is the most accurate field
+                    * specified. Round up adjusting it towards
+                    * future.
+                    */
+                   add_to_field (state, r, -1);
+
+                   /*
+                    * Go back a second if the result is to be used
+                    * for inclusive comparisons.
+                    */
+                   if (round == PARSE_TIME_ROUND_UP_INCLUSIVE)
+                       add_to_field (state, TM_REL_SEC, 1);
+               }
+               round = PARSE_TIME_NO_ROUND; /* No more rounding. */
+           } else {
+               if (f == TM_ABS_MDAY &&
+                   is_field_set (state, TM_REL_WEEK)) {
+                   /* Week is most accurate. */
+                   week_round = round;
+                   round = PARSE_TIME_NO_ROUND;
+               } else {
+                   set_field (state, f, get_field_epoch_value (f));
+               }
+           }
+       }
+
+       if (!is_field_set (state, f))
+           set_field (state, f, tm_get_field (&now, f));
+    }
+
+    /* Special case: rounding with week accuracy. */
+    if (week_round != PARSE_TIME_NO_ROUND) {
+       /* Temporarily set more accurate fields to now. */
+       set_field (state, TM_ABS_SEC, tm_get_field (&now, TM_ABS_SEC));
+       set_field (state, TM_ABS_MIN, tm_get_field (&now, TM_ABS_MIN));
+       set_field (state, TM_ABS_HOUR, tm_get_field (&now, TM_ABS_HOUR));
+       set_field (state, TM_ABS_MDAY, tm_get_field (&now, TM_ABS_MDAY));
+    }
+
+    /*
+     * Set all fields. They may contain out of range values before
+     * normalization by mktime(3).
+     */
+    tm.tm_sec = get_field (state, TM_ABS_SEC) - get_field (state, TM_REL_SEC);
+    tm.tm_min = get_field (state, TM_ABS_MIN) - get_field (state, TM_REL_MIN);
+    tm.tm_hour = get_field (state, TM_ABS_HOUR) - get_field (state, TM_REL_HOUR);
+    tm.tm_mday = get_field (state, TM_ABS_MDAY) -
+                get_field (state, TM_REL_DAY) - 7 * get_field (state, TM_REL_WEEK);
+    tm.tm_mon = get_field (state, TM_ABS_MON) - get_field (state, TM_REL_MON);
+    tm.tm_mon--; /* 1- to 0-based */
+    tm.tm_year = get_field (state, TM_ABS_YEAR) - get_field (state, TM_REL_YEAR) - 1900;
+
+    /*
+     * It's always normal time.
+     *
+     * XXX: This is probably not a solution that universally
+     * works. Just make sure DST is not taken into account. We don't
+     * want rounding to be affected by DST.
+     */
+    tm.tm_isdst = -1;
+
+    /* Special case: rounding with week accuracy. */
+    if (week_round != PARSE_TIME_NO_ROUND) {
+       /* Normalize to get proper tm.wday. */
+       r = normalize_tm (&tm);
+       if (r < 0)
+           return r;
+
+       /* Set more accurate fields back to zero. */
+       tm.tm_sec = 0;
+       tm.tm_min = 0;
+       tm.tm_hour = 0;
+       tm.tm_isdst = -1;
+
+       /* Monday is the true 1st day of week, but this is easier. */
+       if (week_round >= PARSE_TIME_ROUND_UP) {
+           tm.tm_mday += 7 - tm.tm_wday;
+           if (week_round == PARSE_TIME_ROUND_UP_INCLUSIVE)
+               tm.tm_sec--;
+       } else {
+           tm.tm_mday -= tm.tm_wday;
+       }
+    }
+
+    if (is_field_set (state, TM_TZ)) {
+       /* tm is in specified TZ, convert to UTC for timegm(3). */
+       tm.tm_min -= get_field (state, TM_TZ);
+       t = timegm (&tm);
+    } else {
+       /* tm is in local time. */
+       t = mktime (&tm);
+    }
+
+    if (t == (time_t) -1)
+       return -PARSE_TIME_ERR_LIB;
+
+    *t_out = t;
+
+    return 0;
+}
+
+/* Internally, all errors are < 0. parse_time_string() returns errors > 0. */
+#define EXTERNAL_ERR(r) (-r)
+
+int
+parse_time_string (const char *s, time_t *t, const time_t *ref, int round)
+{
+    struct state state = { .last_field = TM_NONE };
+    int r;
+
+    if (!s || !t)
+       return EXTERNAL_ERR (-PARSE_TIME_ERR);
+
+    r = parse_input (&state, s);
+    if (r < 0)
+       return EXTERNAL_ERR (r);
+
+    r = create_output (&state, t, ref, round);
+    if (r < 0)
+       return EXTERNAL_ERR (r);
+
+    return 0;
+}
diff --git a/parse-time-string/parse-time-string.h b/parse-time-string/parse-time-string.h
new file mode 100644 (file)
index 0000000..bfa4ee3
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#ifndef PARSE_TIME_STRING_H
+#define PARSE_TIME_STRING_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <time.h>
+
+/* return values for parse_time_string() */
+enum {
+    PARSE_TIME_OK = 0,
+    PARSE_TIME_ERR,            /* unspecified error */
+    PARSE_TIME_ERR_LIB,                /* library call failed */
+    PARSE_TIME_ERR_ALREADYSET, /* attempt to set unit twice */
+    PARSE_TIME_ERR_FORMAT,     /* generic date/time format error */
+    PARSE_TIME_ERR_DATEFORMAT, /* date format error */
+    PARSE_TIME_ERR_TIMEFORMAT, /* time format error */
+    PARSE_TIME_ERR_INVALIDDATE,        /* date value error */
+    PARSE_TIME_ERR_INVALIDTIME,        /* time value error */
+    PARSE_TIME_ERR_KEYWORD,    /* unknown keyword */
+};
+
+/* round values for parse_time_string() */
+enum {
+    PARSE_TIME_ROUND_DOWN = -1,
+    PARSE_TIME_NO_ROUND = 0,
+    PARSE_TIME_ROUND_UP = 1,
+    PARSE_TIME_ROUND_UP_INCLUSIVE = 2,
+};
+
+/**
+ * parse_time_string() - user friendly date and time parser
+ * @s:         string to parse
+ * @t:         pointer to time_t to store parsed time in
+ * @ref:       pointer to time_t containing reference date/time, or NULL
+ * @round:     PARSE_TIME_NO_ROUND, PARSE_TIME_ROUND_DOWN, or
+ *             PARSE_TIME_ROUND_UP
+ *
+ * Parse a date/time string 's' and store the parsed date/time result
+ * in 't'.
+ *
+ * A reference date/time is used for determining the "date/time units"
+ * (roughly equivalent to struct tm members) not specified by 's'. If
+ * 'ref' is non-NULL, it must contain a pointer to a time_t to be used
+ * as reference date/time. Otherwise, the current time is used.
+ *
+ * If 's' does not specify a full date/time, the 'round' parameter
+ * specifies if and how the result should be rounded as follows:
+ *
+ *   PARSE_TIME_NO_ROUND: All date/time units that are not specified
+ *   by 's' are set to the corresponding unit derived from the
+ *   reference date/time.
+ *
+ *   PARSE_TIME_ROUND_DOWN: All date/time units that are more accurate
+ *   than the most accurate unit specified by 's' are set to the
+ *   smallest valid value for that unit. Rest of the unspecified units
+ *   are set as in PARSE_TIME_NO_ROUND.
+ *
+ *   PARSE_TIME_ROUND_UP: All date/time units that are more accurate
+ *   than the most accurate unit specified by 's' are set to the
+ *   smallest valid value for that unit. The most accurate unit
+ *   specified by 's' is incremented by one (and this is rolled over
+ *   to the less accurate units as necessary), unless the most
+ *   accurate unit is seconds. Rest of the unspecified units are set
+ *   as in PARSE_TIME_NO_ROUND.
+ *
+ *   PARSE_TIME_ROUND_UP_INCLUSIVE: Same as PARSE_TIME_ROUND_UP, minus
+ *   one second, unless the most accurate unit specified by 's' is
+ *   seconds. This is useful for callers that require a value for
+ *   inclusive comparison of the result.
+ *
+ * Return 0 (PARSE_TIME_OK) for succesfully parsed date/time, or one
+ * of PARSE_TIME_ERR_* on error. 't' is not modified on error.
+ */
+int parse_time_string (const char *s, time_t *t, const time_t *ref, int round);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PARSE_TIME_STRING_H */
diff --git a/performance-test/.gitignore b/performance-test/.gitignore
new file mode 100644 (file)
index 0000000..f3f9be4
--- /dev/null
@@ -0,0 +1,4 @@
+tmp.*/
+log.*/
+corpus/
+notmuch.cache.*/
diff --git a/performance-test/M00-new b/performance-test/M00-new
new file mode 100755 (executable)
index 0000000..99c3f52
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+test_description='notmuch new'
+
+. ./perf-test-lib.sh
+
+# ensure initial 'notmuch new' is run by memory_start
+uncache_database
+
+memory_start
+
+# run 'notmuch new' a second time, to test different code paths
+memory_run "notmuch new" "notmuch new"
+
+memory_done
diff --git a/performance-test/M01-dump-restore b/performance-test/M01-dump-restore
new file mode 100755 (executable)
index 0000000..be5894a
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+test_description='dump and restore'
+
+. ./perf-test-lib.sh
+
+memory_start
+
+memory_run 'load nmbug tags' 'notmuch restore --accumulate --input=corpus.tags/nmbug.sup-dump'
+memory_run 'dump *' 'notmuch dump --output=tags.sup'
+memory_run 'restore *' 'notmuch restore --input=tags.sup'
+memory_run 'dump --format=batch-tag *' 'notmuch dump --format=batch-tag --output=tags.bt'
+memory_run 'restore --format=batch-tag *' 'notmuch restore --format=batch-tag --input=tags.bt'
+
+memory_done
diff --git a/performance-test/Makefile b/performance-test/Makefile
new file mode 100644 (file)
index 0000000..de492a7
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/performance-test/Makefile.local b/performance-test/Makefile.local
new file mode 100644 (file)
index 0000000..73aa963
--- /dev/null
@@ -0,0 +1,42 @@
+# -*- makefile -*-
+
+dir := performance-test
+
+include $(dir)/version.sh
+
+TIME_TEST_SCRIPT := ${dir}/notmuch-time-test
+MEMORY_TEST_SCRIPT := ${dir}/notmuch-memory-test
+
+CORPUS_NAME := notmuch-email-corpus-$(PERFTEST_VERSION).tar.xz
+TXZFILE := ${dir}/download/${CORPUS_NAME}
+SIGFILE := ${TXZFILE}.asc
+DEFAULT_URL :=  http://notmuchmail.org/releases/${CORPUS_NAME}
+
+perf-test: time-test memory-test
+
+time-test: setup-perf-test all
+       @echo
+       $(TIME_TEST_SCRIPT) $(OPTIONS)
+
+memory-test: setup-perf-test all
+       @echo
+       $(MEMORY_TEST_SCRIPT) $(OPTIONS)
+
+
+.PHONY: download-corpus setup-perf-test
+
+# Note that this intentionally does not depend on download-corpus.
+setup-perf-test: $(TXZFILE)
+       gpg --verify $(SIGFILE)
+
+$(TXZFILE):
+       @printf "\nPlease download ${TXZFILE} using:\n\n"
+       @printf "\t%% make download-corpus\n\n"
+       @echo or see http://notmuchmail.org/corpus for download locations
+       @echo
+       @false
+
+download-corpus:
+       wget -O ${TXZFILE} ${DEFAULT_URL}
+
+CLEAN := $(CLEAN) $(dir)/tmp.* $(dir)/log.* $(dir)/corpus $(dir)/notmuch.cache.*
diff --git a/performance-test/README b/performance-test/README
new file mode 100644 (file)
index 0000000..996724c
--- /dev/null
@@ -0,0 +1,90 @@
+Performance Tests
+-----------------
+
+This directory contains two kinds of performance tests: time tests,
+and memory tests. The former use gnu time, and the latter use
+valgrind.
+
+Pre-requisites
+--------------
+
+In addition to having notmuch, you need:
+
+- gpg
+- gnu tar
+- gnu time (for the time tests)
+- xz. Some speedup can be gotten by installing "pixz", but this is
+  probably only worthwhile if you are debugging the tests.
+- valgrind (for the memory tests)
+
+Getting set up to run tests:
+----------------------------
+
+First, you need to get the corpus.  If you don't already have the gpg
+key for David Bremner, run
+
+   % gpg --search 'david@tethera.net'
+
+This should get you a key with fingerprint
+
+    815B 6398 2A79 F8E7 C727  86C4 762B 57BB 7842 06AD
+
+(the last 8 digits are printed as the "key id").
+
+To fetch the actual corpus it should work to run
+
+   % make download-corpus
+
+In case that fails or is too slow, check
+
+   http://notmuchmail.org/corpus
+
+for a list of mirrors.
+
+Running tests
+-------------
+
+The easiest way to run performance tests is to say "make perf-test".
+This will run all time and memory tests.  Be aware that the memory
+tests are quite time consuming when run on the full corpus, and that
+depending on your interests it may be more sensible to run "make
+time-test" or "make memory-test".  You can also invoke one of the
+scripts notmuch-time-test or notmuch-memory-test or run a more
+specific subset of tests by simply invoking one of the executable
+scripts in this directory, (such as ./T00-new).  Each test script
+supports the following arguments
+
+--small / --medium / --large   Choose corpus size.
+--debug                                Enable debugging. In particular don't delete
+                               temporary directories.
+
+When using the make targets, you can pass arguments to all test
+scripts by defining the make variable OPTIONS.
+
+Writing tests
+-------------
+
+Have a look at "T01-dump-restore" for an example time test and
+"M00-new" for an example memory test. In both cases sourcing
+"perf-test-lib.sh" is mandatory.
+
+Basics:
+
+- '(time|memory)_start' unpacks the mail corpus and calls notmuch new if it
+   cannot find a cache of the appropriate corpus.
+- '(time|memory)_run' runs the command under time or valgrind. Currently
+  "memory_run" does not support i/o redirection in the command.
+- '(time|memory)_done' does the cleanup; comment it out or pass --debug to the
+  script to leave the temporary files around.
+
+Utility functions include
+
+- 'add_email_corpus' unpacks a set of messages and tags
+- 'cache_database': makes a snapshot of the current database
+- 'uncache_database': forces the next '(time|memory)_start' to rebuild the
+  database.
+
+Scripts are run in the order specified in notmuch-perf-test. In the
+future this order might be chosen automatically so please follow the
+convention of starting the name with 'T' or 'M' followed by two digits
+to specify the order.
diff --git a/performance-test/T00-new b/performance-test/T00-new
new file mode 100755 (executable)
index 0000000..553bb8b
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+test_description='notmuch new'
+
+. ./perf-test-lib.sh
+
+uncache_database
+
+time_start
+
+for i in $(seq 2 6); do
+    time_run "notmuch new #$i" 'notmuch new'
+done
+
+time_done
diff --git a/performance-test/T01-dump-restore b/performance-test/T01-dump-restore
new file mode 100755 (executable)
index 0000000..b2ff940
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+test_description='dump and restore'
+
+. ./perf-test-lib.sh
+
+time_start
+
+time_run 'load nmbug tags' 'notmuch restore --accumulate < corpus.tags/nmbug.sup-dump'
+time_run 'dump *' 'notmuch dump > tags.out'
+time_run 'restore *' 'notmuch restore < tags.out'
+
+time_done
diff --git a/performance-test/T02-tag b/performance-test/T02-tag
new file mode 100755 (executable)
index 0000000..78ceccc
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+test_description='tagging'
+
+. ./perf-test-lib.sh
+
+time_start
+
+time_run 'tag * +new_tag' "notmuch tag +new_tag '*'"
+time_run 'tag * +existing_tag' "notmuch tag +new_tag '*'"
+time_run 'tag * -existing_tag' "notmuch tag -new_tag '*'"
+time_run 'tag * -missing_tag' "notmuch tag -new_tag '*'"
+
+time_done
diff --git a/performance-test/download/.gitignore b/performance-test/download/.gitignore
new file mode 100644 (file)
index 0000000..7b09234
--- /dev/null
@@ -0,0 +1,2 @@
+*.tar.gz
+*.tar.xz
diff --git a/performance-test/download/notmuch-email-corpus-0.3.tar.xz.asc b/performance-test/download/notmuch-email-corpus-0.3.tar.xz.asc
new file mode 100644 (file)
index 0000000..f109e81
--- /dev/null
@@ -0,0 +1,9 @@
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+iJwEAAECAAYFAlC9a90ACgkQTiiN/0Um85nAMAP+LCWdKzolcl/KW+JcCd0Dk+9v
+0vvtBVEhBes0TbK6iWrxCV2OIuYG/RhnFlJTZ4MjgaTRxzDubpC+JktaJdLmIQUN
+B7ZIDMjFduCwmtyLiuu/00CjxJKUXm7vx+ULGpvp0uxFE/vaqGP997BHwBjjfBVm
+YX6BlLX1SV6TfENkuRE=
+=Mks5
+-----END PGP SIGNATURE-----
diff --git a/performance-test/notmuch-time-test b/performance-test/notmuch-time-test
new file mode 100755 (executable)
index 0000000..54a208f
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+# Run tests
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+# Adapted from a Makefile to a shell script by Carl Worth (2010)
+
+if [ ${BASH_VERSINFO[0]} -lt 4 ]; then
+    echo "Error: The notmuch test suite requires a bash version >= 4.0"
+    echo "due to use of associative arrays within the test suite."
+    echo "Please try again with a newer bash (or help us fix the"
+    echo "test suite to be more portable). Thanks."
+    exit 1
+fi
+
+cd $(dirname "$0")
+
+TESTS="
+  T00-new
+  T01-dump-restore
+  T02-tag
+"
+
+for test in $TESTS; do
+    ./$test "$@"
+done
diff --git a/performance-test/perf-test-lib.sh b/performance-test/perf-test-lib.sh
new file mode 100644 (file)
index 0000000..9ee7661
--- /dev/null
@@ -0,0 +1,193 @@
+. ./version.sh
+
+corpus_size=large
+
+while test "$#" -ne 0
+do
+       case "$1" in
+       -d|--debug)
+               debug=t;
+               shift
+               ;;
+       -s|--small)
+               corpus_size=small;
+               shift
+               ;;
+       -m|--medium)
+               corpus_size=medium;
+               shift
+               ;;
+       -l|--large)
+               corpus_size=large;
+               shift
+               ;;
+       *)
+               echo "error: unknown performance test option '$1'" >&2; exit 1 ;;
+       esac
+done
+. ../test/test-lib-common.sh
+
+set -e
+
+if ! test -x ../notmuch
+then
+       echo >&2 'You do not seem to have built notmuch yet.'
+       exit 1
+fi
+
+DB_CACHE_DIR=${TEST_DIRECTORY}/notmuch.cache.$corpus_size
+
+add_email_corpus ()
+{
+    rm -rf ${MAIL_DIR}
+
+    case "$corpus_size" in
+       small)
+           mail_subdir="mail/enron/bailey-s"
+           check_for="${TEST_DIRECTORY}/corpus/$mail_subdir"
+           ;;
+       medium)
+           mail_subdir="mail/notmuch-archive"
+           check_for="${TEST_DIRECTORY}/corpus/$mail_subdir"
+           ;;
+       *)
+           mail_subdir=mail
+           check_for="${TEST_DIRECTORY}/corpus/$mail_subdir/enron/wolfe-j"
+    esac
+
+    MAIL_CORPUS="${TEST_DIRECTORY}/corpus/$mail_subdir"
+    TAG_CORPUS="${TEST_DIRECTORY}/corpus/tags"
+
+    args=()
+    if [ ! -d "$TAG_CORPUS" ] ; then
+       args+=("notmuch-email-corpus/tags")
+    fi
+
+    if [ ! -d "$check_for" ] ; then
+       args+=("notmuch-email-corpus/$mail_subdir")
+    fi
+
+    if [[ ${#args[@]} > 0 ]]; then
+       if command -v pixz > /dev/null; then
+           XZ=pixz
+       else
+           XZ=xz
+       fi
+
+       printf "Unpacking corpus\n"
+       mkdir -p "${TEST_DIRECTORY}/corpus"
+
+       tar --checkpoint=.5000 --extract --strip-components=1 \
+           --directory ${TEST_DIRECTORY}/corpus \
+           --use-compress-program ${XZ} \
+           --file ../download/notmuch-email-corpus-${PERFTEST_VERSION}.tar.xz \
+           "${args[@]}"
+
+       printf "\n"
+
+    fi
+
+    cp -lr $TAG_CORPUS $TMP_DIRECTORY/corpus.tags
+    cp -lr $MAIL_CORPUS $MAIL_DIR
+}
+
+notmuch_new_with_cache ()
+{
+    if [ -d $DB_CACHE_DIR ]; then
+       cp -r $DB_CACHE_DIR ${MAIL_DIR}/.notmuch
+    else
+       "$1" 'Initial notmuch new' "notmuch new"
+       cache_database
+    fi
+}
+
+time_start ()
+{
+    add_email_corpus
+
+    print_header
+
+    notmuch_new_with_cache time_run
+}
+
+memory_start ()
+{
+    add_email_corpus
+
+    local timestamp=$(date +%Y%m%dT%H%M%S)
+    log_dir="${TEST_DIRECTORY}/log.$(basename $0)-$corpus_size-${timestamp}"
+    mkdir -p ${log_dir}
+
+    notmuch_new_with_cache memory_run
+}
+
+memory_run ()
+{
+    test_count=$(($test_count+1))
+
+    log_file=$log_dir/$test_count.log
+    talloc_log=$log_dir/$test_count.talloc
+
+    printf "[ %d ]\t%s\n" $test_count "$1"
+
+    NOTMUCH_TALLOC_REPORT="$talloc_log" valgrind --leak-check=full --log-file="$log_file" $2
+
+    awk '/LEAK SUMMARY/,/suppressed/ { sub(/^==[0-9]*==/," "); print }' "$log_file"
+    echo
+    sed -n -e 's/.*[(]total *\([^)]*\)[)]/talloced at exit: \1/p' $talloc_log
+    echo
+}
+
+memory_done ()
+{
+    time_done
+}
+
+cache_database ()
+{
+    if [ -d $MAIL_DIR/.notmuch ]; then
+       cp -r $MAIL_DIR/.notmuch $DB_CACHE_DIR
+    else
+       echo "Warning: No database found to cache"
+    fi
+}
+
+uncache_database ()
+{
+    rm -rf $DB_CACHE_DIR
+}
+
+print_header ()
+{
+    printf "\t\t\tWall(s)\tUsr(s)\tSys(s)\tRes(K)\tIn/Out(512B)\n"
+}
+
+time_run ()
+{
+    printf "  %-22s" "$1"
+    test_count=$(($test_count+1))
+    if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi
+    if ! eval >&3 "/usr/bin/time -f '%e\t%U\t%S\t%M\t%I/%O' $2" ; then
+       test_failure=$(($test_failure + 1))
+       return 1
+    fi
+    return 0
+}
+
+time_done ()
+{
+    if [ "$test_failure" = "0" ]; then
+       rm -rf "$remove_tmp"
+       exit 0
+    else
+       exit 1
+    fi
+}
+
+cd -P "$test" || error "Cannot setup test environment"
+test_failure=0
+test_count=0
+
+printf "\n%-55s [%s %s]\n"  \
+    "$(basename "$0"): Testing ${test_description:-notmuch performance}" \
+    "${PERFTEST_VERSION}"  "${corpus_size}"
diff --git a/performance-test/version.sh b/performance-test/version.sh
new file mode 100644 (file)
index 0000000..afafc73
--- /dev/null
@@ -0,0 +1,3 @@
+# this should be both a valid Makefile fragment and valid POSIX(ish) shell.
+
+PERFTEST_VERSION=0.3
diff --git a/sprinter-json.c b/sprinter-json.c
new file mode 100644 (file)
index 0000000..0a07790
--- /dev/null
@@ -0,0 +1,201 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+
+struct sprinter_json {
+    struct sprinter vtable;
+    FILE *stream;
+    /* Top of the state stack, or NULL if the printer is not currently
+     * inside any aggregate types. */
+    struct json_state *state;
+
+    /* A flag to signify that a separator should be inserted in the
+     * output as soon as possible.
+     */
+    notmuch_bool_t insert_separator;
+};
+
+struct json_state {
+    struct json_state *parent;
+    /* True if nothing has been printed in this aggregate yet.
+     * Suppresses the comma before a value. */
+    notmuch_bool_t first;
+    /* The character that closes the current aggregate. */
+    char close;
+};
+
+/* Helper function to set up the stream to print a value.  If this
+ * value follows another value, prints a comma. */
+static struct sprinter_json *
+json_begin_value (struct sprinter *sp)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+
+    if (spj->state) {
+       if (! spj->state->first) {
+           fputc (',', spj->stream);
+           if (spj->insert_separator) {
+               fputc ('\n', spj->stream);
+               spj->insert_separator = FALSE;
+           } else {
+               fputc (' ', spj->stream);
+           }
+       } else {
+           spj->state->first = FALSE;
+       }
+    }
+    return spj;
+}
+
+/* Helper function to begin an aggregate type.  Prints the open
+ * character and pushes a new state frame. */
+static void
+json_begin_aggregate (struct sprinter *sp, char open, char close)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+    struct json_state *state = talloc (spj, struct json_state);
+
+    fputc (open, spj->stream);
+    state->parent = spj->state;
+    state->first = TRUE;
+    state->close = close;
+    spj->state = state;
+}
+
+static void
+json_begin_map (struct sprinter *sp)
+{
+    json_begin_aggregate (sp, '{', '}');
+}
+
+static void
+json_begin_list (struct sprinter *sp)
+{
+    json_begin_aggregate (sp, '[', ']');
+}
+
+static void
+json_end (struct sprinter *sp)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+    struct json_state *state = spj->state;
+
+    fputc (spj->state->close, spj->stream);
+    spj->state = state->parent;
+    talloc_free (state);
+    if (spj->state == NULL)
+       fputc ('\n', spj->stream);
+}
+
+/* This implementation supports embedded NULs as allowed by the JSON
+ * specification and Unicode.  Support for *parsing* embedded NULs
+ * varies, but is generally not a problem outside of C-based parsers
+ * (Python's json module and Emacs' json.el take embedded NULs in
+ * stride). */
+static void
+json_string_len (struct sprinter *sp, const char *val, size_t len)
+{
+    static const char *const escapes[] = {
+       ['\"'] = "\\\"", ['\\'] = "\\\\", ['\b'] = "\\b",
+       ['\f'] = "\\f",  ['\n'] = "\\n",  ['\t'] = "\\t"
+    };
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fputc ('"', spj->stream);
+    for (; len; ++val, --len) {
+       unsigned char ch = *val;
+       if (ch < ARRAY_SIZE (escapes) && escapes[ch])
+           fputs (escapes[ch], spj->stream);
+       else if (ch >= 32)
+           fputc (ch, spj->stream);
+       else
+           fprintf (spj->stream, "\\u%04x", ch);
+    }
+    fputc ('"', spj->stream);
+}
+
+static void
+json_string (struct sprinter *sp, const char *val)
+{
+    if (val == NULL)
+       val = "";
+    json_string_len (sp, val, strlen (val));
+}
+
+static void
+json_integer (struct sprinter *sp, int val)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fprintf (spj->stream, "%d", val);
+}
+
+static void
+json_boolean (struct sprinter *sp, notmuch_bool_t val)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fputs (val ? "true" : "false", spj->stream);
+}
+
+static void
+json_null (struct sprinter *sp)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fputs ("null", spj->stream);
+}
+
+static void
+json_map_key (struct sprinter *sp, const char *key)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+
+    json_string (sp, key);
+    fputs (": ", spj->stream);
+    spj->state->first = TRUE;
+}
+
+static void
+json_set_prefix (unused (struct sprinter *sp), unused (const char *name))
+{
+}
+
+static void
+json_separator (struct sprinter *sp)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+
+    spj->insert_separator = TRUE;
+}
+
+struct sprinter *
+sprinter_json_create (const void *ctx, FILE *stream)
+{
+    static const struct sprinter_json template = {
+       .vtable = {
+           .begin_map = json_begin_map,
+           .begin_list = json_begin_list,
+           .end = json_end,
+           .string = json_string,
+           .string_len = json_string_len,
+           .integer = json_integer,
+           .boolean = json_boolean,
+           .null = json_null,
+           .map_key = json_map_key,
+           .separator = json_separator,
+           .set_prefix = json_set_prefix,
+           .is_text_printer = FALSE,
+       }
+    };
+    struct sprinter_json *res;
+
+    res = talloc (ctx, struct sprinter_json);
+    if (! res)
+       return NULL;
+
+    *res = template;
+    res->stream = stream;
+    return &res->vtable;
+}
diff --git a/sprinter-sexp.c b/sprinter-sexp.c
new file mode 100644 (file)
index 0000000..0aa51e8
--- /dev/null
@@ -0,0 +1,236 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2012 Peter Feigl
+ *
+ * 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: Peter Feigl <peter.feigl@gmx.at>
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+#include <ctype.h>
+
+struct sprinter_sexp {
+    struct sprinter vtable;
+    FILE *stream;
+    /* Top of the state stack, or NULL if the printer is not currently
+     * inside any aggregate types. */
+    struct sexp_state *state;
+
+    /* A flag to signify that a separator should be inserted in the
+     * output as soon as possible. */
+    notmuch_bool_t insert_separator;
+};
+
+struct sexp_state {
+    struct sexp_state *parent;
+
+    /* True if nothing has been printed in this aggregate yet.
+     * Suppresses the space before a value. */
+    notmuch_bool_t first;
+};
+
+/* Helper function to set up the stream to print a value.  If this
+ * value follows another value, prints a space. */
+static struct sprinter_sexp *
+sexp_begin_value (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+
+    if (sps->state) {
+       if (! sps->state->first) {
+           if (sps->insert_separator) {
+               fputc ('\n', sps->stream);
+               sps->insert_separator = FALSE;
+           } else {
+               fputc (' ', sps->stream);
+           }
+       } else {
+           sps->state->first = FALSE;
+       }
+    }
+    return sps;
+}
+
+/* Helper function to begin an aggregate type.  Prints the open
+ * character and pushes a new state frame. */
+static void
+sexp_begin_aggregate (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+    struct sexp_state *state = talloc (sps, struct sexp_state);
+
+    fputc ('(', sps->stream);
+    state->parent = sps->state;
+    state->first = TRUE;
+    sps->state = state;
+}
+
+static void
+sexp_begin_map (struct sprinter *sp)
+{
+    sexp_begin_aggregate (sp);
+}
+
+static void
+sexp_begin_list (struct sprinter *sp)
+{
+    sexp_begin_aggregate (sp);
+}
+
+static void
+sexp_end (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+    struct sexp_state *state = sps->state;
+
+    fputc (')', sps->stream);
+    sps->state = state->parent;
+    talloc_free (state);
+    if (sps->state == NULL)
+       fputc ('\n', sps->stream);
+}
+
+static void
+sexp_string_len (struct sprinter *sp, const char *val, size_t len)
+{
+    /* Some characters need escaping. " and \ work fine in all Lisps,
+     * \n is not supported in CL, but all others work fine.
+     * Characters below 32 are printed as \123o (three-digit
+     * octals), which work fine in most Schemes and Emacs. */
+    static const char *const escapes[] = {
+       ['\"'] = "\\\"", ['\\'] = "\\\\",  ['\n'] = "\\n"
+    };
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fputc ('"', sps->stream);
+    for (; len; ++val, --len) {
+       unsigned char ch = *val;
+       if (ch < ARRAY_SIZE (escapes) && escapes[ch])
+           fputs (escapes[ch], sps->stream);
+       else if (ch >= 32)
+           fputc (ch, sps->stream);
+       else
+           fprintf (sps->stream, "\\%03o", ch);
+    }
+    fputc ('"', sps->stream);
+}
+
+static void
+sexp_string (struct sprinter *sp, const char *val)
+{
+    if (val == NULL)
+       val = "";
+    sexp_string_len (sp, val, strlen (val));
+}
+
+/* Prints a symbol, i.e. the name preceded by a colon. This should work
+ * in all Lisps, at least as a symbol, if not as a proper keyword */
+static void
+sexp_keyword (struct sprinter *sp, const char *val)
+{
+    unsigned int i = 0;
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+    char ch;
+
+    if (val == NULL)
+       INTERNAL_ERROR ("illegal symbol NULL");
+
+    for (i = 0; i < strlen (val); i++) {
+       ch = val[i];
+       if (! (isalnum (ch) || (ch == '-') || (ch == '_'))) {
+           INTERNAL_ERROR ("illegal character in symbol %s: %c", val, ch);
+       }
+    }
+    fputc (':', sps->stream);
+    fputs (val, sps->stream);
+}
+
+static void
+sexp_integer (struct sprinter *sp, int val)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fprintf (sps->stream, "%d", val);
+}
+
+static void
+sexp_boolean (struct sprinter *sp, notmuch_bool_t val)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fputs (val ? "t" : "nil", sps->stream);
+}
+
+static void
+sexp_null (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fputs ("nil", sps->stream);
+}
+
+static void
+sexp_map_key (struct sprinter *sp, const char *key)
+{
+    sexp_begin_value (sp);
+
+    sexp_keyword (sp, key);
+}
+
+static void
+sexp_set_prefix (unused (struct sprinter *sp), unused (const char *name))
+{
+}
+
+static void
+sexp_separator (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+
+    sps->insert_separator = TRUE;
+}
+
+struct sprinter *
+sprinter_sexp_create (const void *ctx, FILE *stream)
+{
+    static const struct sprinter_sexp template = {
+       .vtable = {
+           .begin_map = sexp_begin_map,
+           .begin_list = sexp_begin_list,
+           .end = sexp_end,
+           .string = sexp_string,
+           .string_len = sexp_string_len,
+           .integer = sexp_integer,
+           .boolean = sexp_boolean,
+           .null = sexp_null,
+           .map_key = sexp_map_key,
+           .separator = sexp_separator,
+           .set_prefix = sexp_set_prefix,
+           .is_text_printer = FALSE,
+       }
+    };
+    struct sprinter_sexp *res;
+
+    res = talloc (ctx, struct sprinter_sexp);
+    if (! res)
+       return NULL;
+
+    *res = template;
+    res->stream = stream;
+    return &res->vtable;
+}
diff --git a/sprinter-text.c b/sprinter-text.c
new file mode 100644 (file)
index 0000000..7779488
--- /dev/null
@@ -0,0 +1,157 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+
+/* "Structured printer" interface for unstructured text printing.
+ * Note that --output=summary is dispatched and formatted in
+ * notmuch-search.c, the code in this file is only used for all other
+ * output types.
+ */
+
+struct sprinter_text {
+    struct sprinter vtable;
+    FILE *stream;
+
+    /* The current prefix to be printed with string/integer/boolean
+     * data.
+     */
+    const char *current_prefix;
+
+    /* A flag to indicate if this is the first tag. Used in list of tags
+     * for summary.
+     */
+    notmuch_bool_t first_tag;
+};
+
+static void
+text_string_len (struct sprinter *sp, const char *val, size_t len)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    if (sptxt->current_prefix != NULL)
+       fprintf (sptxt->stream, "%s:", sptxt->current_prefix);
+
+    fwrite (val, len, 1, sptxt->stream);
+}
+
+static void
+text_string (struct sprinter *sp, const char *val)
+{
+    if (val == NULL)
+       val = "";
+    text_string_len (sp, val, strlen (val));
+}
+
+static void
+text_integer (struct sprinter *sp, int val)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fprintf (sptxt->stream, "%d", val);
+}
+
+static void
+text_boolean (struct sprinter *sp, notmuch_bool_t val)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fputs (val ? "true" : "false", sptxt->stream);
+}
+
+static void
+text_separator (struct sprinter *sp)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fputc ('\n', sptxt->stream);
+}
+
+static void
+text0_separator (struct sprinter *sp)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fputc ('\0', sptxt->stream);
+}
+
+static void
+text_set_prefix (struct sprinter *sp, const char *prefix)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    sptxt->current_prefix = prefix;
+}
+
+/* The structure functions begin_map, begin_list, end and map_key
+ * don't do anything in the text formatter.
+ */
+
+static void
+text_begin_map (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_begin_list (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_end (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_null (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_map_key (unused (struct sprinter *sp), unused (const char *key))
+{
+}
+
+struct sprinter *
+sprinter_text_create (const void *ctx, FILE *stream)
+{
+    static const struct sprinter_text template = {
+       .vtable = {
+           .begin_map = text_begin_map,
+           .begin_list = text_begin_list,
+           .end = text_end,
+           .string = text_string,
+           .string_len = text_string_len,
+           .integer = text_integer,
+           .boolean = text_boolean,
+           .null = text_null,
+           .map_key = text_map_key,
+           .separator = text_separator,
+           .set_prefix = text_set_prefix,
+           .is_text_printer = TRUE,
+       },
+    };
+    struct sprinter_text *res;
+
+    res = talloc (ctx, struct sprinter_text);
+    if (! res)
+       return NULL;
+
+    *res = template;
+    res->stream = stream;
+    return &res->vtable;
+}
+
+struct sprinter *
+sprinter_text0_create (const void *ctx, FILE *stream)
+{
+    struct sprinter *sp;
+
+    sp = sprinter_text_create (ctx, stream);
+    if (! sp)
+       return NULL;
+
+    sp->separator = text0_separator;
+
+    return sp;
+}
diff --git a/sprinter.h b/sprinter.h
new file mode 100644 (file)
index 0000000..f859672
--- /dev/null
@@ -0,0 +1,84 @@
+#ifndef NOTMUCH_SPRINTER_H
+#define NOTMUCH_SPRINTER_H
+
+/* Necessary for notmuch_bool_t */
+#include "notmuch-client.h"
+
+/* Structure printer interface. This is used to create output
+ * structured as maps (with key/value pairs), lists and primitives
+ * (strings, integers and booleans).
+ */
+typedef struct sprinter {
+    /* Start a new map/dictionary structure. This should be followed by
+     * a sequence of alternating calls to map_key and one of the
+     * value-printing functions until the map is ended by end.
+     */
+    void (*begin_map) (struct sprinter *);
+
+    /* Start a new list/array structure.
+     */
+    void (*begin_list) (struct sprinter *);
+
+    /* End the last opened list or map structure.
+     */
+    void (*end) (struct sprinter *);
+
+    /* Print one string/integer/boolean/null element (possibly inside
+     * a list or map, followed or preceded by separators).  For string
+     * and string_len, the char * must be UTF-8 encoded.  string_len
+     * allows non-terminated strings and strings with embedded NULs
+     * (though the handling of the latter is format-dependent). For
+     * string (but not string_len) the string pointer passed may be
+     * NULL.
+     */
+    void (*string) (struct sprinter *, const char *);
+    void (*string_len) (struct sprinter *, const char *, size_t);
+    void (*integer) (struct sprinter *, int);
+    void (*boolean) (struct sprinter *, notmuch_bool_t);
+    void (*null) (struct sprinter *);
+
+    /* Print the key of a map's key/value pair. The char * must be UTF-8
+     * encoded.
+     */
+    void (*map_key) (struct sprinter *, const char *);
+
+    /* Insert a separator (usually extra whitespace). For the text
+     * printers, this is a syntactic separator. For the structured
+     * printers, this is for improved readability without affecting
+     * the abstract syntax of the structure being printed. For JSON,
+     * this could simply be a line break.
+     */
+    void (*separator) (struct sprinter *);
+
+    /* Set the current string prefix. This only affects the text
+     * printer, which will print this string, followed by a colon,
+     * before any string. For other printers, this does nothing.
+     */
+    void (*set_prefix) (struct sprinter *, const char *);
+
+    /* True if this is the special-cased plain text printer.
+     */
+    notmuch_bool_t is_text_printer;
+} sprinter_t;
+
+
+/* Create a new unstructured printer that emits the default text format
+ * for "notmuch search". */
+struct sprinter *
+sprinter_text_create (const void *ctx, FILE *stream);
+
+/* Create a new unstructured printer that emits the text format for
+ * "notmuch search", with each field separated by a null character
+ * instead of the newline character. */
+struct sprinter *
+sprinter_text0_create (const void *ctx, FILE *stream);
+
+/* Create a new structure printer that emits JSON. */
+struct sprinter *
+sprinter_json_create (const void *ctx, FILE *stream);
+
+/* Create a new structure printer that emits S-Expressions. */
+struct sprinter *
+sprinter_sexp_create (const void *ctx, FILE *stream);
+
+#endif // NOTMUCH_SPRINTER_H
diff --git a/tag-util.c b/tag-util.c
new file mode 100644 (file)
index 0000000..701d329
--- /dev/null
@@ -0,0 +1,439 @@
+#include <assert.h>
+#include "string-util.h"
+#include "tag-util.h"
+#include "hex-escape.h"
+
+#define TAG_OP_LIST_INITIAL_SIZE 10
+
+struct _tag_operation_t {
+    const char *tag;
+    notmuch_bool_t remove;
+};
+
+struct _tag_op_list_t {
+    tag_operation_t *ops;
+    size_t count;
+    size_t size;
+};
+
+static tag_parse_status_t
+line_error (tag_parse_status_t status,
+           const char *line,
+           const char *format, ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    fprintf (stderr, status < 0 ? "Error: " : "Warning: ");
+    vfprintf (stderr, format, va_args);
+    fprintf (stderr, " [%s]\n", line);
+    return status;
+}
+
+/*
+ * Test tags for some forbidden cases.
+ *
+ * return: NULL if OK,
+ *        explanatory message otherwise.
+ */
+
+static const char *
+illegal_tag (const char *tag, notmuch_bool_t remove)
+{
+
+    if (*tag == '\0' && ! remove)
+       return "empty tag forbidden";
+
+    /* This disallows adding tags starting with "-", in particular the
+     * non-removable tag "-" and enables notmuch tag to take long
+     * options more easily.
+     */
+
+    if (*tag == '-' && ! remove)
+       return "tag starting with '-' forbidden";
+
+    return NULL;
+}
+
+tag_parse_status_t
+parse_tag_line (void *ctx, char *line,
+               tag_op_flag_t flags,
+               char **query_string,
+               tag_op_list_t *tag_ops)
+{
+    char *tok = line;
+    size_t tok_len = 0;
+    char *line_for_error;
+    tag_parse_status_t ret = TAG_PARSE_SUCCESS;
+
+    chomp_newline (line);
+
+    line_for_error = talloc_strdup (ctx, line);
+    if (line_for_error == NULL) {
+       fprintf (stderr, "Error: out of memory\n");
+       return TAG_PARSE_OUT_OF_MEMORY;
+    }
+
+    /* remove leading space */
+    while (*tok == ' ' || *tok == '\t')
+       tok++;
+
+    /* Skip empty and comment lines. */
+    if (*tok == '\0' || *tok == '#') {
+       ret = TAG_PARSE_SKIPPED;
+       goto DONE;
+    }
+
+    tag_op_list_reset (tag_ops);
+
+    /* Parse tags. */
+    while ((tok = strtok_len (tok + tok_len, " ", &tok_len)) != NULL) {
+       notmuch_bool_t remove;
+       char *tag;
+
+       /* Optional explicit end of tags marker. */
+       if (tok_len == 2 && strncmp (tok, "--", tok_len) == 0) {
+           tok = strtok_len (tok + tok_len, " ", &tok_len);
+           if (tok == NULL) {
+               ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                                 "no query string after --");
+               goto DONE;
+           }
+           break;
+       }
+
+       /* Implicit end of tags. */
+       if (*tok != '-' && *tok != '+')
+           break;
+
+       /* If tag is terminated by NUL, there's no query string. */
+       if (*(tok + tok_len) == '\0') {
+           ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                             "no query string");
+           goto DONE;
+       }
+
+       /* Terminate, and start next token after terminator. */
+       *(tok + tok_len++) = '\0';
+
+       remove = (*tok == '-');
+       tag = tok + 1;
+
+       /* Maybe refuse illegal tags. */
+       if (! (flags & TAG_FLAG_BE_GENEROUS)) {
+           const char *msg = illegal_tag (tag, remove);
+           if (msg) {
+               ret = line_error (TAG_PARSE_INVALID, line_for_error, msg);
+               goto DONE;
+           }
+       }
+
+       /* Decode tag. */
+       if (hex_decode_inplace (tag) != HEX_SUCCESS) {
+           ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                             "hex decoding of tag %s failed", tag);
+           goto DONE;
+       }
+
+       if (tag_op_list_append (tag_ops, tag, remove)) {
+           ret = line_error (TAG_PARSE_OUT_OF_MEMORY, line_for_error,
+                             "aborting");
+           goto DONE;
+       }
+    }
+
+    if (tok == NULL) {
+       /* use a different error message for testing */
+       ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                         "missing query string");
+       goto DONE;
+    }
+
+    /* tok now points to the query string */
+    *query_string = tok;
+
+  DONE:
+    talloc_free (line_for_error);
+    return ret;
+}
+
+tag_parse_status_t
+parse_tag_command_line (void *ctx, int argc, char **argv,
+                       char **query_str, tag_op_list_t *tag_ops)
+{
+
+    int i;
+
+    tag_op_list_reset (tag_ops);
+
+    for (i = 0; i < argc; i++) {
+       if (strcmp (argv[i], "--") == 0) {
+           i++;
+           break;
+       }
+
+       if (argv[i][0] != '+' && argv[i][0] != '-')
+           break;
+
+       notmuch_bool_t is_remove = argv[i][0] == '-';
+       const char *msg;
+
+       msg = illegal_tag (argv[i] + 1, is_remove);
+       if (msg) {
+           fprintf (stderr, "Error: %s", msg);
+           return TAG_PARSE_INVALID;
+       }
+
+       tag_op_list_append (tag_ops, argv[i] + 1, is_remove);
+    }
+
+    if (tag_op_list_size (tag_ops) == 0) {
+       fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
+       return TAG_PARSE_INVALID;
+    }
+
+    *query_str = query_string_from_args (ctx, argc - i, &argv[i]);
+
+    if (*query_str == NULL || **query_str == '\0') {
+       fprintf (stderr, "Error: notmuch tag requires at least one search term.\n");
+       return TAG_PARSE_INVALID;
+    }
+
+    return TAG_PARSE_SUCCESS;
+}
+
+
+static inline void
+message_error (notmuch_message_t *message,
+              notmuch_status_t status,
+              const char *format, ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    vfprintf (stderr, format, va_args);
+    fprintf (stderr, "Message-ID: %s\n", notmuch_message_get_message_id (message));
+    fprintf (stderr, "Status: %s\n", notmuch_status_to_string (status));
+}
+
+static int
+makes_changes (notmuch_message_t *message,
+              tag_op_list_t *list,
+              tag_op_flag_t flags)
+{
+
+    size_t i;
+
+    notmuch_tags_t *tags;
+    notmuch_bool_t changes = FALSE;
+
+    /* First, do we delete an existing tag? */
+    changes = FALSE;
+    for (tags = notmuch_message_get_tags (message);
+        ! changes && notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       const char *cur_tag = notmuch_tags_get (tags);
+       int last_op =  (flags & TAG_FLAG_REMOVE_ALL) ? -1 : 0;
+
+       /* scan backwards to get last operation */
+       i = list->count;
+       while (i > 0) {
+           i--;
+           if (strcmp (cur_tag, list->ops[i].tag) == 0) {
+               last_op = list->ops[i].remove ? -1 : 1;
+               break;
+           }
+       }
+
+       changes = (last_op == -1);
+    }
+    notmuch_tags_destroy (tags);
+
+    if (changes)
+       return TRUE;
+
+    /* Now check for adding new tags */
+    for (i = 0; i < list->count; i++) {
+       notmuch_bool_t exists = FALSE;
+
+       if (list->ops[i].remove)
+           continue;
+
+       for (tags = notmuch_message_get_tags (message);
+            notmuch_tags_valid (tags);
+            notmuch_tags_move_to_next (tags)) {
+           const char *cur_tag = notmuch_tags_get (tags);
+           if (strcmp (cur_tag, list->ops[i].tag) == 0) {
+               exists = TRUE;
+               break;
+           }
+       }
+       notmuch_tags_destroy (tags);
+
+       /* the following test is conservative,
+        * in the sense it ignores cases like +foo ... -foo
+        * but this is OK from a correctness point of view
+        */
+       if (! exists)
+           return TRUE;
+    }
+    return FALSE;
+
+}
+
+notmuch_status_t
+tag_op_list_apply (notmuch_message_t *message,
+                  tag_op_list_t *list,
+                  tag_op_flag_t flags)
+{
+    size_t i;
+    notmuch_status_t status = 0;
+    tag_operation_t *tag_ops = list->ops;
+
+    if (! (flags & TAG_FLAG_PRE_OPTIMIZED) && ! makes_changes (message, list, flags))
+       return NOTMUCH_STATUS_SUCCESS;
+
+    status = notmuch_message_freeze (message);
+    if (status) {
+       message_error (message, status, "freezing message");
+       return status;
+    }
+
+    if (flags & TAG_FLAG_REMOVE_ALL) {
+       status = notmuch_message_remove_all_tags (message);
+       if (status) {
+           message_error (message, status, "removing all tags");
+           return status;
+       }
+    }
+
+    for (i = 0; i < list->count; i++) {
+       if (tag_ops[i].remove) {
+           status = notmuch_message_remove_tag (message, tag_ops[i].tag);
+           if (status) {
+               message_error (message, status, "removing tag %s", tag_ops[i].tag);
+               return status;
+           }
+       } else {
+           status = notmuch_message_add_tag (message, tag_ops[i].tag);
+           if (status) {
+               message_error (message, status, "adding tag %s", tag_ops[i].tag);
+               return status;
+           }
+
+       }
+    }
+
+    status = notmuch_message_thaw (message);
+    if (status) {
+       message_error (message, status, "thawing message");
+       return status;
+    }
+
+
+    if (flags & TAG_FLAG_MAILDIR_SYNC) {
+       status = notmuch_message_tags_to_maildir_flags (message);
+       if (status) {
+           message_error (message, status, "synching tags to maildir");
+           return status;
+       }
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+
+}
+
+
+/* Array of tagging operations (add or remove.  Size will be increased
+ * as necessary. */
+
+tag_op_list_t *
+tag_op_list_create (void *ctx)
+{
+    tag_op_list_t *list;
+
+    list = talloc (ctx, tag_op_list_t);
+    if (list == NULL)
+       return NULL;
+
+    list->size = TAG_OP_LIST_INITIAL_SIZE;
+    list->count = 0;
+
+    list->ops = talloc_array (list, tag_operation_t, list->size);
+    if (list->ops == NULL)
+       return NULL;
+
+    return list;
+}
+
+
+int
+tag_op_list_append (tag_op_list_t *list,
+                   const char *tag,
+                   notmuch_bool_t remove)
+{
+    /* Make room if current array is full.  This should be a fairly
+     * rare case, considering the initial array size.
+     */
+
+    if (list->count == list->size) {
+       list->size *= 2;
+       list->ops = talloc_realloc (list, list->ops, tag_operation_t,
+                                   list->size);
+       if (list->ops == NULL) {
+           fprintf (stderr, "Out of memory.\n");
+           return 1;
+       }
+    }
+
+    /* add the new operation */
+
+    list->ops[list->count].tag = tag;
+    list->ops[list->count].remove = remove;
+    list->count++;
+    return 0;
+}
+
+/*
+ *   Is the i'th tag operation a remove?
+ */
+
+notmuch_bool_t
+tag_op_list_isremove (const tag_op_list_t *list, size_t i)
+{
+    assert (i < list->count);
+    return list->ops[i].remove;
+}
+
+/*
+ * Reset a list to contain no operations
+ */
+
+void
+tag_op_list_reset (tag_op_list_t *list)
+{
+    list->count = 0;
+}
+
+/*
+ * Return the number of operations in a list
+ */
+
+size_t
+tag_op_list_size (const tag_op_list_t *list)
+{
+    return list->count;
+}
+
+/*
+ *   return the i'th tag in the list
+ */
+
+const char *
+tag_op_list_tag (const tag_op_list_t *list, size_t i)
+{
+    assert (i < list->count);
+    return list->ops[i].tag;
+}
diff --git a/tag-util.h b/tag-util.h
new file mode 100644 (file)
index 0000000..246de85
--- /dev/null
@@ -0,0 +1,149 @@
+#ifndef _TAG_UTIL_H
+#define _TAG_UTIL_H
+
+#include "notmuch-client.h"
+
+typedef struct _tag_operation_t tag_operation_t;
+typedef struct _tag_op_list_t tag_op_list_t;
+
+/* Use powers of 2 */
+typedef enum {
+    TAG_FLAG_NONE = 0,
+
+    /* Operations are synced to maildir, if possible.
+     */
+    TAG_FLAG_MAILDIR_SYNC = (1 << 0),
+
+    /* Remove all tags from message before applying list.
+     */
+    TAG_FLAG_REMOVE_ALL = (1 << 1),
+
+    /* Don't try to avoid database operations. Useful when we
+     * know that message passed needs these operations.
+     */
+    TAG_FLAG_PRE_OPTIMIZED = (1 << 2),
+
+    /* Accept strange tags that might be user error;
+     * intended for use by notmuch-restore.
+     */
+    TAG_FLAG_BE_GENEROUS = (1 << 3)
+
+} tag_op_flag_t;
+
+/* These should obey the convention that fatal errors are negative,
+ * skipped lines are positive.
+ */
+typedef enum {
+    TAG_PARSE_OUT_OF_MEMORY = -1,
+
+    /* Line parsed successfuly. */
+    TAG_PARSE_SUCCESS = 0,
+
+    /* Line has a syntax error */
+    TAG_PARSE_INVALID = 1,
+
+    /* Line was blank or a comment */
+    TAG_PARSE_SKIPPED = 2
+
+} tag_parse_status_t;
+
+/* Parse a string of the following format:
+ *
+ * +<tag>|-<tag> [...] [--] <search-terms>
+ *
+ * Each line is interpreted similarly to "notmuch tag" command line
+ * arguments. The delimiter is one or more spaces ' '. Any characters
+ * in <tag> and <search-terms> MAY be hex encoded with %NN where NN is
+ * the hexadecimal value of the character. Any ' ' and '%' characters
+ * in <tag> and <search-terms> MUST be hex encoded (using %20 and %25,
+ * respectively). Any characters that are not part of <tag> or
+ * <search-terms> MUST NOT be hex encoded.
+ *
+ * Leading and trailing space ' ' is ignored. Empty lines and lines
+ * beginning with '#' are ignored.
+ *
+ *
+ * Output Parameters:
+ *     ops     contains a list of tag operations
+ *     query_str the search terms.
+ */
+tag_parse_status_t
+parse_tag_line (void *ctx, char *line,
+               tag_op_flag_t flags,
+               char **query_str, tag_op_list_t *ops);
+
+
+
+/* Parse a command line of the following format:
+ *
+ * +<tag>|-<tag> [...] [--] <search-terms>
+ *
+ * Output Parameters:
+ *     ops     contains a list of tag operations
+ *     query_str the search terms.
+ */
+
+tag_parse_status_t
+parse_tag_command_line (void *ctx, int argc, char **argv,
+                       char **query_str, tag_op_list_t *ops);
+
+/*
+ * Create an empty list of tag operations
+ *
+ * ctx is passed to talloc
+ */
+
+tag_op_list_t *
+tag_op_list_create (void *ctx);
+
+/*
+ * Add a tag operation (delete iff remove == TRUE) to a list.
+ * The list is expanded as necessary.
+ */
+
+int
+tag_op_list_append (tag_op_list_t *list,
+                   const char *tag,
+                   notmuch_bool_t remove);
+
+/*
+ * Apply a list of tag operations, in order, to a given message.
+ *
+ * Flags can be bitwise ORed; see enum above for possibilies.
+ */
+
+notmuch_status_t
+tag_op_list_apply (notmuch_message_t *message,
+                  tag_op_list_t *tag_ops,
+                  tag_op_flag_t flags);
+
+/*
+ * Return the number of operations in a list
+ */
+
+size_t
+tag_op_list_size (const tag_op_list_t *list);
+
+/*
+ * Reset a list to contain no operations
+ */
+
+void
+tag_op_list_reset (tag_op_list_t *list);
+
+
+/*
+ *   return the i'th tag in the list
+ */
+
+const char *
+tag_op_list_tag (const tag_op_list_t *list, size_t i);
+
+/*
+ *   Is the i'th tag operation a remove?
+ */
+
+notmuch_bool_t
+tag_op_list_isremove (const tag_op_list_t *list, size_t i);
+
+#endif
index e63c689dcd1a895cbdcb042787d22a1c2d3e3266..97e0248787288e5c68039c7b48c5c5859f8c4fbd 100644 (file)
@@ -3,4 +3,7 @@ corpus.mail
 smtp-dummy
 symbol-test
 arg-test
+hex-xcode
+random-corpus
+parse-time
 tmp.*
index 4a6a4b12e862253c3964ec203aa45734825d3f19..2ec659560626e1a16e224d85e1b58cb318033fe9 100644 (file)
@@ -13,15 +13,36 @@ smtp_dummy_modules = $(smtp_dummy_srcs:.c=.o)
 $(dir)/arg-test: $(dir)/arg-test.o command-line-arguments.o util/libutil.a
        $(call quiet,CC) -I. $^ -o $@
 
+$(dir)/hex-xcode: $(dir)/hex-xcode.o command-line-arguments.o util/libutil.a
+       $(call quiet,CC) -I. $^ -o $@ -ltalloc
+
+random_corpus_deps =  $(dir)/random-corpus.o  $(dir)/database-test.o \
+                       notmuch-config.o command-line-arguments.o \
+                       lib/libnotmuch.a util/libutil.a \
+                       parse-time-string/libparse-time-string.a
+
+$(dir)/random-corpus: $(random_corpus_deps)
+       $(call quiet,CXX) $(CFLAGS_FINAL) $^ -o $@ $(CONFIGURE_LDFLAGS)
+
 $(dir)/smtp-dummy: $(smtp_dummy_modules)
        $(call quiet,CC) $^ -o $@
 
 $(dir)/symbol-test: $(dir)/symbol-test.o
-       $(call quiet,CXX) $^ -o $@ -Llib -lnotmuch -lxapian
+       $(call quiet,CXX) $^ -o $@ -Llib -lnotmuch $(XAPIAN_LDFLAGS)
+
+$(dir)/parse-time: $(dir)/parse-time.o parse-time-string/parse-time-string.o
+       $(call quiet,CC) $^ -o $@
 
 .PHONY: test check
 
-test-binaries: $(dir)/arg-test $(dir)/smtp-dummy $(dir)/symbol-test
+TEST_BINARIES=$(dir)/arg-test \
+             $(dir)/hex-xcode \
+             $(dir)/random-corpus \
+             $(dir)/parse-time \
+             $(dir)/smtp-dummy \
+             $(dir)/symbol-test
+
+test-binaries: $(TEST_BINARIES)
 
 test:  all test-binaries
        @${dir}/notmuch-test $(OPTIONS)
@@ -31,4 +52,9 @@ check: test
 SRCS := $(SRCS) $(smtp_dummy_srcs)
 CLEAN := $(CLEAN) $(dir)/smtp-dummy $(dir)/smtp-dummy.o \
         $(dir)/symbol-test $(dir)/symbol-test.o \
-        $(dir)/arg-test $(dir)/arg-test.o
+        $(dir)/arg-test $(dir)/arg-test.o \
+        $(dir)/hex-xcode $(dir)/hex-xcode.o \
+        $(dir)/database-test.o \
+        $(dir)/random-corpus $(dir)/random-corpus.o \
+        $(dir)/parse-time $(dir)/parse-time.o \
+        $(dir)/corpus.mail $(dir)/test-results $(dir)/tmp.*
index 43656a35baf9f9bc5e5d41aca2b65c8e4e763450..81c232ddd0a3fc90e1dbaa74f34f1252da004193 100644 (file)
@@ -56,7 +56,7 @@ The following command-line options are available when running tests:
        run the tests with this option in parallel.
 
 --root=<dir>::
-       This runs the testsuites specified under a seperate directory.
+       This runs the testsuites specified under a separate directory.
        However, caution is advised, as not all tests are maintained
        with this relocation in mind, so some tests may behave
        differently.
@@ -69,12 +69,12 @@ can be specified as follows:
 
        make test OPTIONS="--verbose"
 
-You can choose an emacs binary to run the tests in one of the
-following ways.
+You can choose an emacs binary (and corresponding emacsclient) to run
+the tests in one of the following ways.
 
-       TEST_EMACS=my-special-emacs make test
-       TEST_EMACS=my-special-emacs ./emacs
-       make test TEST_EMACS=my-special-emacs
+       TEST_EMACS=my-special-emacs TEST_EMACSCLIENT=my-emacsclient make test
+       TEST_EMACS=my-special-emacs TEST_EMACSCLIENT=my-emacsclient ./emacs
+       make test TEST_EMACS=my-special-emacs TEST_EMACSCLIENT=my-emacsclient
 
 Skipping Tests
 --------------
@@ -176,12 +176,12 @@ library for your script to use.
    will generate a failure and print the difference of the two
    strings.
 
- test_expect_equal_file <output> <expected>
+ test_expect_equal_file <file1> <file2>
 
-   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.
+   Identical to test_exepect_equal, except that <file1> and <file2>
+   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>
 
index adc56e30e7d97e449a328ed8ff7642b429084b7b..6c49eacd7f3a9bab7537ee82059b6c66a5a76d7a 100644 (file)
@@ -41,7 +41,7 @@ int main(int argc, char **argv){
        printf("positional arg 1 %s\n", pos_arg1);
 
     if (pos_arg2)
-       printf("positional arg 2 %s\n", pos_arg1);
+       printf("positional arg 2 %s\n", pos_arg2);
 
 
     for ( ; opt_index < argc ; opt_index ++) {
index 672de0b306696afeda8a7ba6751b64a8ddbd9d0d..94e90874d2e0e422c433e0601b9e88f0342a618e 100755 (executable)
@@ -9,7 +9,7 @@ keyword 1
 int 7
 string foo
 positional arg 1 pos1
-positional arg 2 pos1
+positional arg 2 pos2
 EOF
 test_expect_equal_file OUTPUT EXPECTED
 
index 6df0a00e4084281c9b03027668ff2e0ca38bf15a..1c786fa2724262dad40cc9f7b883b6269586bc1d 100755 (executable)
@@ -51,7 +51,7 @@ if test_require_external_prereq gdb; then
 
     # 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 -a $MAIL_DIR $MAIL_DIR.snap
     cp ${NOTMUCH_CONFIG} ${NOTMUCH_CONFIG}.snap
     NOTMUCH_CONFIG=${NOTMUCH_CONFIG}.snap notmuch config set database.path $MAIL_DIR.snap
 
index d6aed24c0e292135b35b8b938a374311838c257f..1b2a7d20d8f31a58e87c7d94e48048f49c852644 100755 (executable)
@@ -53,9 +53,17 @@ test_expect_code 2 'failure to clean up causes the test to fail' '
 test_begin_subtest 'Ensure that all available tests will be run by notmuch-test'
 eval $(sed -n -e '/^TESTS="$/,/^"$/p' $TEST_DIRECTORY/notmuch-test)
 tests_in_suite=$(for i in $TESTS; do echo $i; done | sort)
-available=$(find "$TEST_DIRECTORY" -maxdepth 1 -type f -executable -printf '%f\n' | \
-    sed -r -e "/^(aggregate-results.sh|notmuch-test|smtp-dummy|test-verbose|symbol-test|arg-test)$/d" | \
-    sort)
+available=$(find "$TEST_DIRECTORY" -maxdepth 1 -type f -perm +111  \
+    ! -name aggregate-results.sh       \
+    ! -name arg-test                   \
+    ! -name hex-xcode                  \
+    ! -name notmuch-test               \
+    ! -name parse-time                 \
+    ! -name random-corpus              \
+    ! -name smtp-dummy                 \
+    ! -name symbol-test                        \
+    ! -name test-verbose               \
+    | sed 's,.*/,,' | sort)
 test_expect_equal "$tests_in_suite" "$available"
 
 EXPECTED=$TEST_DIRECTORY/test.expected-output
@@ -73,7 +81,7 @@ test_begin_subtest "Ensure that -v does not suppress test output"
 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-*
+rm $TEST_DIRECTORY/test-results/test-verbose
 rm -r $TEST_DIRECTORY/tmp.test-verbose
 test_expect_equal "$output" "$expected"
 
index 93ecb13997768194777d52f886505df4ebc29b76..cfa1f327b94c4d1e96481afcd15d2f4692353f86 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 test_description='"notmuch config"'
-. test-lib.sh
+. ./test-lib.sh
 
 test_begin_subtest "Get string value"
 test_expect_equal "$(notmuch config get user.name)" "Notmuch Test Suite"
index 300b171419734d649a41e2a8a0a567dd879f8d26..879b114a35f567bb190185581cf6bad3d6ea28c4 100755 (executable)
@@ -4,37 +4,38 @@ test_description='"notmuch count" for messages and threads'
 
 add_email_corpus
 
-SEARCH="\"*\""
+# Note: The 'wc -l' results below are wrapped in arithmetic evaluation
+# $((...)) to strip whitespace. This is for portability, as 'wc -l'
+# emits whitespace on some BSD variants.
 
 test_begin_subtest "message count is the default for notmuch count"
 test_expect_equal \
-    "`notmuch search --output=messages ${SEARCH} | wc -l`" \
-    "`notmuch count ${SEARCH}`"
+    "$((`notmuch search --output=messages '*' | wc -l`))" \
+    "`notmuch count '*'`"
 
 test_begin_subtest "message count with --output=messages"
 test_expect_equal \
-    "`notmuch search --output=messages ${SEARCH} | wc -l`" \
-    "`notmuch count --output=messages ${SEARCH}`"
+    "$((`notmuch search --output=messages '*' | wc -l`))" \
+    "`notmuch count --output=messages '*'`"
 
 test_begin_subtest "thread count with --output=threads"
 test_expect_equal \
-    "`notmuch search --output=threads ${SEARCH} | wc -l`" \
-    "`notmuch count --output=threads ${SEARCH}`"
+    "$((`notmuch search --output=threads '*' | wc -l`))" \
+    "`notmuch count --output=threads '*'`"
 
 test_begin_subtest "thread count is the default for notmuch search"
 test_expect_equal \
-    "`notmuch search ${SEARCH} | wc -l`" \
-    "`notmuch count --output=threads ${SEARCH}`"
+    "$((`notmuch search '*' | wc -l`))" \
+    "`notmuch count --output=threads '*'`"
 
-SEARCH="from:cworth and not from:cworth"
 test_begin_subtest "count with no matching messages"
 test_expect_equal \
     "0" \
-    "`notmuch count --output=messages ${SEARCH}`"
+    "`notmuch count --output=messages from:cworth and not from:cworth`"
 
 test_begin_subtest "count with no matching threads"
 test_expect_equal \
     "0" \
-    "`notmuch count --output=threads ${SEARCH}`"
+    "`notmuch count --output=threads from:cworth and not from:cworth`"
 
 test_done
index be752b19674cdd174e27cee6ead6a53bc52cc172..aa96ec228ca082cb8f7a125064883a2f76b7ae58 100755 (executable)
@@ -51,8 +51,7 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Date": "Sat,
- 01 Jan 2000 12:00:00 +0000"},
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "good",
  "fingerprint": "'$FINGERPRINT'",
@@ -62,9 +61,10 @@ expected='[[[{"id": "XXXXX",
  "content-type": "text/plain",
  "content": "This is a test signed message.\n"},
  {"id": 3,
- "content-type": "application/pgp-signature"}]}]},
+ "content-type": "application/pgp-signature",
+ "content-length": 315}]}]},
  []]]]'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 
@@ -85,8 +85,7 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Date": "Sat,
- 01 Jan 2000 12:00:00 +0000"},
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "good",
  "fingerprint": "'$FINGERPRINT'",
@@ -97,9 +96,10 @@ expected='[[[{"id": "XXXXX",
  "content-type": "text/plain",
  "content": "This is a test signed message.\n"},
  {"id": 3,
- "content-type": "application/pgp-signature"}]}]},
+ "content-type": "application/pgp-signature",
+ "content-length": 315}]}]},
  []]]]'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 
@@ -119,8 +119,7 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Date": "Sat,
- 01 Jan 2000 12:00:00 +0000"},
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "error",
  "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'",
@@ -130,9 +129,10 @@ expected='[[[{"id": "XXXXX",
  "content-type": "text/plain",
  "content": "This is a test signed message.\n"},
  {"id": 3,
- "content-type": "application/pgp-signature"}]}]},
+ "content-type": "application/pgp-signature",
+ "content-length": 315}]}]},
  []]]]'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 mv "${GNUPGHOME}"{.bak,}
@@ -193,14 +193,14 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test encrypted message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Date": "Sat,
- 01 Jan 2000 12:00:00 +0000"},
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "encstatus": [{"status": "good"}],
  "sigstatus": [],
  "content-type": "multipart/encrypted",
  "content": [{"id": 2,
- "content-type": "application/pgp-encrypted"},
+ "content-type": "application/pgp-encrypted",
+ "content-length": 11},
  {"id": 3,
  "content-type": "multipart/mixed",
  "content": [{"id": 4,
@@ -208,9 +208,11 @@ expected='[[[{"id": "XXXXX",
  "content": "This is a test encrypted message.\n"},
  {"id": 5,
  "content-type": "application/octet-stream",
+ "content-length": 28,
+ "content-transfer-encoding": "base64",
  "filename": "TESTATTACHMENT"}]}]}]},
  []]]]'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 
@@ -221,7 +223,7 @@ output=$(notmuch show --format=json --part=4 --decrypt subject:"test encrypted m
 expected='{"id": 4,
  "content-type": "text/plain",
  "content": "This is a test encrypted message.\n"}'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 
@@ -235,9 +237,11 @@ test_expect_equal_file OUTPUT TESTATTACHMENT
 
 test_begin_subtest "decryption failure with missing key"
 mv "${GNUPGHOME}"{,.bak}
+# The length of the encrypted attachment varies so must be normalized.
 output=$(notmuch show --format=json --decrypt subject:"test encrypted message 001" \
     | notmuch_json_show_sanitize \
-    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|' \
+    | sed -e 's|"content-length": 6[1234567890]*|"content-length": 652|')
 expected='[[[{"id": "XXXXX",
  "match": true,
  "excluded": false,
@@ -248,17 +252,18 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test encrypted message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Date": "Sat,
- 01 Jan 2000 12:00:00 +0000"},
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "encstatus": [{"status": "bad"}],
  "content-type": "multipart/encrypted",
  "content": [{"id": 2,
- "content-type": "application/pgp-encrypted"},
+ "content-type": "application/pgp-encrypted",
+ "content-length": 11},
  {"id": 3,
- "content-type": "application/octet-stream"}]}]},
+ "content-type": "application/octet-stream",
+ "content-length": 652}]}]},
  []]]]'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 mv "${GNUPGHOME}"{.bak,}
@@ -283,8 +288,7 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test encrypted message 002",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Date": "Sat,
- 01 Jan 2000 12:00:00 +0000"},
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "encstatus": [{"status": "good"}],
  "sigstatus": [{"status": "good",
@@ -293,12 +297,13 @@ expected='[[[{"id": "XXXXX",
  "userid": " Notmuch Test Suite <test_suite@notmuchmail.org> (INSECURE!)"}],
  "content-type": "multipart/encrypted",
  "content": [{"id": 2,
- "content-type": "application/pgp-encrypted"},
+ "content-type": "application/pgp-encrypted",
+ "content-length": 11},
  {"id": 3,
  "content-type": "text/plain",
  "content": "This is another test encrypted message.\n"}]}]},
  []]]]'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 
@@ -338,8 +343,7 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Date": "Sat,
- 01 Jan 2000 12:00:00 +0000"},
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "error",
  "keyid": "6D92612D94E46381",
@@ -349,9 +353,10 @@ expected='[[[{"id": "XXXXX",
  "content-type": "text/plain",
  "content": "This is a test signed message.\n"},
  {"id": 3,
- "content-type": "application/pgp-signature"}]}]},
+ "content-type": "application/pgp-signature",
+ "content-length": 315}]}]},
  []]]]'
-test_expect_equal \
+test_expect_equal_json \
     "$output" \
     "$expected"
 
diff --git a/test/database-test.c b/test/database-test.c
new file mode 100644 (file)
index 0000000..b8c3a67
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Database routines intended only for testing, not exported from
+ * library.
+ *
+ * Copyright (c) 2012 David Bremner
+ *
+ * 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: David Bremner <david@tethera.net>
+ */
+
+#include "notmuch-private.h"
+#include "database-test.h"
+
+notmuch_status_t
+notmuch_database_add_stub_message (notmuch_database_t *notmuch,
+                                  const char *message_id,
+                                  const char **tags)
+{
+    const char **tag;
+    notmuch_status_t ret;
+    notmuch_private_status_t private_status;
+    notmuch_message_t *message;
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    message = _notmuch_message_create_for_message_id (notmuch,
+                                                     message_id,
+                                                     &private_status);
+    if (message == NULL) {
+       return COERCE_STATUS (private_status,
+                             "Unexpected status value from _notmuch_message_create_for_message_id");
+
+    }
+
+    if (private_status != NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
+       return NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+
+    _notmuch_message_add_term (message, "type", "mail");
+
+    if (tags) {
+       ret = notmuch_message_freeze (message);
+       if (ret)
+           return ret;
+
+       for (tag = tags; *tag; tag++) {
+           ret = notmuch_message_add_tag (message, *tag);
+           if (ret)
+               return ret;
+       }
+
+       ret = notmuch_message_thaw (message);
+       if (ret)
+           return ret;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
diff --git a/test/database-test.h b/test/database-test.h
new file mode 100644 (file)
index 0000000..84f7988
--- /dev/null
@@ -0,0 +1,21 @@
+#ifndef _DATABASE_TEST_H
+#define _DATABASE_TEST_H
+/* Add a new stub message to the given notmuch database.
+ *
+ * At least the following return values are possible:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message
+ *     ID as another message already in the database.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so no message can be added.
+ */
+
+notmuch_status_t
+notmuch_database_add_stub_message (notmuch_database_t *database,
+                                  const char *message_id,
+                                  const char **tag_list);
+
+#endif
index 439e99808af5721c0042e4e9b7ede85cb0b9170a..0004438db493514fed25c95fe244c0eab1767a06 100755 (executable)
@@ -19,7 +19,7 @@ test_expect_success 'Dumping all tags II' \
 
 test_expect_success 'Clearing all tags' \
   'sed -e "s/(\([^(]*\))$/()/" < dump.expected > clear.expected &&
-  notmuch restore clear.expected &&
+  notmuch restore --input=clear.expected &&
   notmuch dump > clear.actual &&
   test_cmp clear.expected clear.actual'
 
@@ -30,7 +30,7 @@ test_expect_success 'Accumulate original tags' \
   test_cmp dump-ABC_DEF.expected dump.actual'
 
 test_expect_success 'Restoring original tags' \
-  'notmuch restore dump.expected &&
+  'notmuch restore --input=dump.expected &&
   notmuch dump > dump.actual &&
   test_cmp dump.expected dump.actual'
 
@@ -39,30 +39,33 @@ test_expect_success 'Restore with nothing to do' \
   notmuch dump > dump.actual &&
   test_cmp dump.expected dump.actual'
 
-test_expect_success 'Restore with nothing to do, II' \
-  'notmuch restore --accumulate dump.expected &&
+test_expect_success 'Accumulate with existing tags' \
+  'notmuch restore --accumulate --input=dump.expected &&
   notmuch dump > dump.actual &&
   test_cmp dump.expected dump.actual'
 
-test_expect_success 'Restore with nothing to do, III' \
+test_expect_success 'Accumulate with no tags' \
   'notmuch restore --accumulate < clear.expected &&
   notmuch dump > dump.actual &&
   test_cmp dump.expected dump.actual'
 
+test_expect_success 'Accumulate with new tags' \
+  'notmuch restore --input=dump.expected &&
+  notmuch restore --accumulate --input=dump-ABC_DEF.expected &&
+  notmuch dump >  OUTPUT.$test_count &&
+  notmuch restore --input=dump.expected &&
+  test_cmp dump-ABC_DEF.expected OUTPUT.$test_count'
+
 # notmuch restore currently only considers the first argument.
 test_expect_success 'Invalid restore invocation' \
-  'test_must_fail notmuch restore dump.expected another_one'
+  'test_must_fail notmuch restore --input=dump.expected another_one'
 
-test_begin_subtest "dump outfile"
-notmuch dump dump-outfile.actual
+test_begin_subtest "dump --output=outfile"
+notmuch dump --output=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_begin_subtest "dump --output=outfile --"
+notmuch dump --output=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
@@ -74,12 +77,217 @@ 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_begin_subtest "dump --output=outfile from:cworth"
+notmuch dump --output=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_begin_subtest "dump --output=outfile -- from:cworth"
+notmuch dump --output=dump-outfile-dash-inbox.actual -- from:cworth
 test_expect_equal_file dump-cworth.expected dump-outfile-dash-inbox.actual
 
+test_begin_subtest "Check for a safe set of message-ids"
+notmuch search --output=messages from:cworth | sed s/^id:// > EXPECTED
+notmuch search --output=messages from:cworth | sed s/^id:// |\
+       $TEST_DIRECTORY/hex-xcode --direction=encode > OUTPUT
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "format=batch-tag, dump sanity check."
+notmuch dump --format=sup from:cworth | cut -f1 -d' ' | \
+    sort > EXPECTED.$test_count
+notmuch dump --format=batch-tag from:cworth | sed 's/^.*-- id://' | \
+    sort > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "format=batch-tag, # round-trip"
+notmuch dump --format=sup | sort > EXPECTED.$test_count
+notmuch dump --format=batch-tag | notmuch restore --format=batch-tag
+notmuch dump --format=sup | sort > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "format=batch-tag, # blank lines and comments"
+notmuch dump --format=batch-tag| sort > EXPECTED.$test_count
+notmuch restore <<EOF
+# this line is a comment; the next has only white space
+        
+
+# the previous line is empty
+EOF
+notmuch dump --format=batch-tag | sort > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "format=batch-tag, # reverse-round-trip empty tag"
+cat <<EOF >EXPECTED.$test_count
++ -- id:20091117232137.GA7669@griffis1.net
+EOF
+notmuch restore --format=batch-tag < EXPECTED.$test_count
+notmuch dump --format=batch-tag id:20091117232137.GA7669@griffis1.net > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+tag1='comic_swear=$&^%$^%\\//-+$^%$'
+enc1=$($TEST_DIRECTORY/hex-xcode --direction=encode "$tag1")
+
+tag2=$(printf 'this\n tag\t has\n spaces')
+enc2=$($TEST_DIRECTORY/hex-xcode --direction=encode "$tag2")
+
+enc3='%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a'
+tag3=$($TEST_DIRECTORY/hex-xcode --direction=decode $enc3)
+
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag +"$tag1" +"$tag2" +"$tag3" -inbox -unread "*"
+
+# initial segment of file used for several tests below.
+cat <<EOF > comments-and-blanks
+# this is a comment
+
+# next line has leading whitespace
+       
+
+EOF
+
+test_begin_subtest 'restoring empty file is not an error'
+notmuch restore < /dev/null 2>OUTPUT.$test_count
+cp /dev/null EXPECTED
+test_expect_equal_file EXPECTED OUTPUT.$test_count
+
+test_begin_subtest 'file of comments and blank lines is not an error'
+notmuch restore --input=comments-and-blanks
+ret_val=$?
+test_expect_equal "$ret_val" "0"
+
+cp comments-and-blanks leading-comments-blanks-batch-tag
+echo "+some_tag -- id:yun1vjwegii.fsf@aiko.keithp.com" \
+    >> leading-comments-blanks-batch-tag
+
+test_begin_subtest 'detect format=batch-tag with leading comments and blanks'
+notmuch restore --input=leading-comments-blanks-batch-tag
+notmuch search --output=tags id:yun1vjwegii.fsf@aiko.keithp.com > OUTPUT.$test_count
+echo "some_tag" > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT.$test_count
+
+cp comments-and-blanks leading-comments-blanks-sup
+echo "yun1vjwegii.fsf@aiko.keithp.com (another_tag)" \
+    >> leading-comments-blanks-sup
+
+test_begin_subtest 'detect format=sup with leading comments and blanks'
+notmuch restore --input=leading-comments-blanks-sup
+notmuch search --output=tags id:yun1vjwegii.fsf@aiko.keithp.com > OUTPUT.$test_count
+echo "another_tag" > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT.$test_count
+
+test_begin_subtest 'format=batch-tag, round trip with strange tags'
+notmuch dump --format=batch-tag > EXPECTED.$test_count
+notmuch dump --format=batch-tag | notmuch restore --format=batch-tag
+notmuch dump --format=batch-tag > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'format=batch-tag, checking encoded output'
+notmuch dump --format=batch-tag -- from:cworth |\
+        awk "{ print \"+$enc1 +$enc2 +$enc3 -- \" \$5 }" > EXPECTED.$test_count
+notmuch dump --format=batch-tag -- from:cworth  > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'restoring sane tags'
+notmuch restore --format=batch-tag < BACKUP
+notmuch dump --format=batch-tag > OUTPUT.$test_count
+test_expect_equal_file BACKUP OUTPUT.$test_count
+
+test_begin_subtest 'format=batch-tag, restore=auto'
+notmuch dump --format=batch-tag > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore --format=auto < EXPECTED.$test_count
+notmuch dump --format=batch-tag > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'format=sup, restore=auto'
+notmuch dump --format=sup > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore --format=auto < EXPECTED.$test_count
+notmuch dump --format=sup > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'format=batch-tag, restore=default'
+notmuch dump --format=batch-tag > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore < EXPECTED.$test_count
+notmuch dump --format=batch-tag > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'format=sup, restore=default'
+notmuch dump --format=sup > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore < EXPECTED.$test_count
+notmuch dump --format=sup > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'restore: checking error messages'
+notmuch restore <<EOF 2>OUTPUT
+# the next line has a space
+a
++0
++a +b
+# trailing whitespace
++a +b 
++c +d --
+# this is a harmless comment, do not yell about it.
+
+# the previous line was blank; also no yelling please
++%zz -- id:whatever
++e +f id:"
++e +f tag:abc
+# the next non-comment line should report an an empty tag error for
+# batch tagging, but not for restore
++ +e -- id:20091117232137.GA7669@griffis1.net
+# valid id, but warning about missing message
++e id:missing_message_id
+# exercise parser
++e -- id:some)stuff
++e -- id:some stuff
++e -- id:some"stuff
++e -- id:"a_message_id_with""_a_quote"
++e -- id:"a message id with spaces"
++e --  id:an_id_with_leading_and_trailing_ws \
+
+EOF
+
+cat <<EOF > EXPECTED
+Warning: cannot parse query: a (skipping)
+Warning: no query string [+0]
+Warning: no query string [+a +b]
+Warning: missing query string [+a +b ]
+Warning: no query string after -- [+c +d --]
+Warning: hex decoding of tag %zz failed [+%zz -- id:whatever]
+Warning: cannot parse query: id:" (skipping)
+Warning: not an id query: tag:abc (skipping)
+Warning: cannot apply tags to missing message: missing_message_id
+Warning: cannot parse query: id:some)stuff (skipping)
+Warning: cannot parse query: id:some stuff (skipping)
+Warning: cannot apply tags to missing message: some"stuff
+Warning: cannot apply tags to missing message: a_message_id_with"_a_quote
+Warning: cannot apply tags to missing message: a message id with spaces
+Warning: cannot apply tags to missing message: an_id_with_leading_and_trailing_ws
+EOF
+
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'roundtripping random message-ids and tags'
+
+    ${TEST_DIRECTORY}/random-corpus --config-path=${NOTMUCH_CONFIG} \
+                       --num-messages=100
+
+     notmuch dump --format=batch-tag| \
+        sort > EXPECTED.$test_count
+
+     notmuch tag +this_tag_is_very_unlikely_to_be_random '*'
+
+     notmuch restore --format=batch-tag < EXPECTED.$test_count
+
+     notmuch dump --format=batch-tag| \
+        sort > OUTPUT.$test_count
+
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
 test_done
+
+# Note the database is "poisoned" for sup format at this point.
index e9f954c37d5cfa985ed144e82c572c97cd027c56..f033bdf5d2d9fd2822946058bd38355421eab734 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 test_description="emacs interface"
-. test-lib.sh
+. ./test-lib.sh
 
 EXPECTED=$TEST_DIRECTORY/emacs.expected-output
 
@@ -35,6 +35,16 @@ test_emacs '(notmuch-search "tag:inbox")
            (test-output)'
 test_expect_equal_file OUTPUT $EXPECTED/notmuch-search-tag-inbox
 
+test_begin_subtest "Incremental parsing of search results"
+test_emacs "(ad-enable-advice 'notmuch-search-process-filter 'around 'pessimal)
+           (ad-activate 'notmuch-search-process-filter)
+           (notmuch-search \"tag:inbox\")
+           (notmuch-test-wait)
+           (ad-disable-advice 'notmuch-search-process-filter 'around 'pessimal)
+           (ad-activate 'notmuch-search-process-filter)
+           (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))
@@ -112,18 +122,30 @@ test_emacs "(notmuch-search \"$os_x_darwin_thread\")
 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_begin_subtest "notmuch-show: add single tag to single message"
 test_emacs "(notmuch-show \"$os_x_darwin_thread\")
            (execute-kbd-macro \"+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_begin_subtest "notmuch-show: remove single tag from single message"
 test_emacs "(notmuch-show \"$os_x_darwin_thread\")
            (execute-kbd-macro \"-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 "notmuch-show: add multiple tags to single message"
+test_emacs "(notmuch-show \"$os_x_darwin_thread\")
+           (execute-kbd-macro \"+tag1-from-show-view +tag2-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 tag1-from-show-view tag2-from-show-view unread)"
+
+test_begin_subtest "notmuch-show: remove multiple tags from single message"
+test_emacs "(notmuch-show \"$os_x_darwin_thread\")
+           (execute-kbd-macro \"-tag1-from-show-view -tag2-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\"")
@@ -159,7 +181,8 @@ emacs_deliver_message \
      (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
+    -e s',^Message-ID: <.*>$,Message-ID: <XXX>,' \
+    -e s',^\(Content-Type: text/plain\); charset=us-ascii$,\1,' < sent_message >OUTPUT
 cat <<EOF >EXPECTED
 From: Notmuch Test Suite <test_suite@notmuchmail.org>
 To: user@example.com
@@ -168,7 +191,7 @@ Date: 01 Jan 2000 12:00:00 -0000
 User-Agent: Notmuch/XXX Emacs/XXX
 Message-ID: <XXX>
 MIME-Version: 1.0
-Content-Type: text/plain; charset=us-ascii
+Content-Type: text/plain
 
 This is a test that messages are sent via SMTP
 EOF
@@ -505,6 +528,32 @@ 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 "Save 8bit attachment from within emacs using notmuch-show-save-attachments"
+
+add_message '[subject]="Attachment with 8bit chars"' \
+       '[header]="MIME-Version: 1.0"' \
+       '[content-type]="multipart/mixed; boundary=\"abcd\""' \
+       '[body]="--abcd
+Content-Type: text/plain
+
+Attachment follows:
+
+--abcd
+Content-Type: application/octet-stream; name=\"sample\"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename=\"sample\"
+
+“¡ Hey ! It compiles ¡ Ship it !”
+
+--abcd--
+"'
+test_emacs '(notmuch-show "id:'"${gen_msg_id}"'")
+           (delete-file "OUTPUT")
+           (let ((standard-input "\"OUTPUT\""))
+             (notmuch-show-save-attachments))'
+
+test_expect_equal "$(cat OUTPUT)" '“¡ Hey ! It compiles ¡ Ship it !”'
+
 test_begin_subtest "View raw message within emacs"
 test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
            (notmuch-show-view-raw-message)
@@ -582,6 +631,44 @@ test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.
            (test-visible-output)'
 test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-with-hidden-messages
 
+test_begin_subtest "notmuch-show: show message headers"
+test_emacs \
+       '(let ((notmuch-message-headers '\''("Subject" "To" "Cc" "Date"))
+              (notmuch-message-headers-visible t))
+          (notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+          (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-message-with-headers-visible
+
+test_begin_subtest "notmuch-show: hide message headers"
+test_emacs \
+       '(let ((notmuch-message-headers '\''("Subject" "To" "Cc" "Date"))
+              (notmuch-message-headers-visible nil))
+          (notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+          (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-message-with-headers-hidden
+
+test_begin_subtest "notmuch-show: hide message headers (w/ notmuch-show-toggle-visibility-headers)"
+test_emacs \
+       '(let ((notmuch-message-headers '\''("Subject" "To" "Cc" "Date"))
+              (notmuch-message-headers-visible t))
+          (notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+          (notmuch-show-toggle-visibility-headers)
+          (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-message-with-headers-hidden
+
+test_begin_subtest "notmuch-show: collapse all messages in thread"
+test_emacs '(notmuch-show "id:f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com")
+       (let ((current-prefix-arg t))
+         (notmuch-show-open-or-close-all)
+         (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-with-all-messages-collapsed
+
+test_begin_subtest "notmuch-show: uncollapse all messages in thread"
+test_emacs '(notmuch-show "id:f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com")
+       (notmuch-show-open-or-close-all)
+       (test-visible-output)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-with-all-messages-uncollapsed
+
 test_begin_subtest "Stashing in notmuch-show"
 add_message '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \
     '[from]="Some One <someone@somewhere.org>"' \
@@ -618,7 +705,7 @@ Some One <someone@somewhere.org>
 Some One Else <notsomeone@somewhere.org>
 Notmuch <notmuch@notmuchmail.org>
 Stash my stashables
-id:"bought"
+id:bought
 bought
 inbox,stashtest
 ${gen_msg_filename}
@@ -642,6 +729,8 @@ test_expect_equal "$(cat OUTPUT)" "thread:XXX"
 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-show \"$message2\")
+           (test-output \"EXPECTED\")"
 test_emacs "(notmuch-search \"$message1 or $message2\")
            (notmuch-test-wait)
            (notmuch-search-show-thread)
@@ -649,8 +738,6 @@ test_emacs "(notmuch-search \"$message1 or $message2\")
            (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"
@@ -738,4 +825,64 @@ counter=$(test_emacs \
 )
 test_expect_equal "$counter" 2
 
+
+add_message '[subject]="HTML mail with images"' \
+    '[content-type]="multipart/related; boundary=abcd"' \
+    '[body]="--abcd
+Content-Type: text/html
+
+<img src="cid:330@goomoji.gmail"> smiley
+
+--abcd
+Content-Type: image/gif
+Content-Transfer-Encoding: base64
+Content-ID: <330@goomoji.gmail>
+
+R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMl
+WLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7
+--abcd--"'
+test_emacs "(let ((mm-text-html-renderer
+                  (if (assq 'shr mm-text-html-renderer-alist)
+                      'shr 'html2text)))
+             (notmuch-show \"id:${gen_msg_id}\"))
+           (test-output)" > /dev/null
+# Different Emacs versions and renderers give very different results,
+# so just check that something reasonable showed up.  We first cat the
+# output so the test framework will print it if the test fails.
+test_expect_success "Rendering HTML mail with images" \
+    'cat OUTPUT && grep -q smiley OUTPUT'
+
+
+test_begin_subtest "Search handles subprocess errors"
+cat > notmuch_fail <<EOF
+#!/bin/sh
+echo This is output
+echo This is an error >&2
+exit 1
+EOF
+chmod a+x notmuch_fail
+test_emacs "(let ((notmuch-command \"$PWD/notmuch_fail\"))
+              (with-current-buffer \"*Messages*\" (erase-buffer))
+              (notmuch-search \"tag:inbox\")
+              (notmuch-test-wait)
+              (with-current-buffer \"*Messages*\"
+                 (test-output \"MESSAGES\"))
+              (with-current-buffer \"*Notmuch errors*\"
+                 (test-output \"ERROR\"))
+              (test-output))"
+sed -i -e 's/^\[.*\]$/[XXX]/' ERROR
+test_expect_equal "$(cat OUTPUT; echo ---; cat MESSAGES; echo ---; cat ERROR)" "\
+Error: Unexpected output from notmuch search:
+This is output
+Error: Unexpected output from notmuch search:
+This is an error
+End of search results.
+---
+$PWD/notmuch_fail exited with status 1 (see *Notmuch errors* for more details)
+---
+[XXX]
+$PWD/notmuch_fail exited with status 1
+command: $PWD/notmuch_fail search --format\=json --format-version\=1 --sort\=newest-first tag\:inbox
+exit status: 1"
+
 test_done
index 6ddde5c6ca9ca8abf0f5f18dc3adab563eef8984..04723467e80814b26f574b8af71284d102250273 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 test_description="emacs address cleaning"
-. test-lib.sh
+. ./test-lib.sh
 
 test_begin_subtest "notmuch-test-address-clean part 1"
 test_emacs_expect_t '(notmuch-test-address-cleaning-1)'
index a998dc4fd7b97be802a01dbe4bb4859d31aca4ff..f7296166a7bdd84b8762b7028a27e3bd88ace808 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
-test_description="Testing emacs notmuch-hello view"
-. test-lib.sh
+test_description="emacs notmuch-hello view"
+. ./test-lib.sh
 
 EXPECTED=$TEST_DIRECTORY/emacs.expected-output
 
index 4351e33eebac445df95de4120f40fe25ff803ee0..9dcbef5e652d054531c75fe3d2a88bfb00d03d97 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 test_description="Emacs with large search results buffer"
-. test-lib.sh
+. ./test-lib.sh
 
 x=xxxxxxxxxx # 10
 x=$x$x$x$x$x$x$x$x$x$x # 100
@@ -36,7 +36,7 @@ test_emacs '(notmuch-search "--this-option-does-not-exist")
 cat <<EOF >EXPECTED
 Error: Unexpected output from notmuch search:
 Unrecognized option: --this-option-does-not-exist
-End of search results. (process returned 1)
+End of search results.
 EOF
 test_expect_equal_file OUTPUT EXPECTED
 
index 5700d2e707a50bdd2ff7fd07fd694976fe517681..9f2ccb0e54fb86182e0448a035a9c0f07fa059ac 100755 (executable)
@@ -1,7 +1,11 @@
 #!/usr/bin/env bash
 
-test_description="Testing emacs notmuch-show view"
-. test-lib.sh
+test_description="emacs notmuch-show view"
+. ./test-lib.sh
+
+EXPECTED=$TEST_DIRECTORY/emacs-show.expected-output
+
+add_email_corpus
 
 test_begin_subtest "Hiding Original Message region at beginning of a message"
 message_id='OriginalMessageHiding.1@notmuchmail.org'
@@ -24,4 +28,173 @@ test_emacs "(notmuch-show \"id:$message_id\")
            (test-visible-output)"
 test_expect_equal_file OUTPUT EXPECTED
 
+test_begin_subtest "Bare subject #1"
+output=$(test_emacs '(notmuch-show-strip-re "Re: subject")')
+test_expect_equal "$output" '"subject"'
+
+test_begin_subtest "Bare subject #2"
+output=$(test_emacs '(notmuch-show-strip-re "re:Re: re:  Re:  re:subject")')
+test_expect_equal "$output" '"subject"'
+
+test_begin_subtest "Bare subject #3"
+output=$(test_emacs '(notmuch-show-strip-re "the cure: fix the regexp")')
+test_expect_equal "$output" '"the cure: fix the regexp"'
+
+test_begin_subtest "don't process cryptographic MIME parts"
+test_emacs '(let ((notmuch-crypto-process-mime nil))
+       (notmuch-show "id:20091117203301.GV3165@dottiness.seas.harvard.edu")
+       (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-process-crypto-mime-parts-off
+
+test_begin_subtest "process cryptographic MIME parts"
+test_emacs '(let ((notmuch-crypto-process-mime t))
+       (notmuch-show "id:20091117203301.GV3165@dottiness.seas.harvard.edu")
+       (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-process-crypto-mime-parts-on
+
+test_begin_subtest "process cryptographic MIME parts (w/ notmuch-show-toggle-process-crypto)"
+test_emacs '(let ((notmuch-crypto-process-mime nil))
+       (notmuch-show "id:20091117203301.GV3165@dottiness.seas.harvard.edu")
+       (notmuch-show-toggle-process-crypto)
+       (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-process-crypto-mime-parts-on
+
+test_begin_subtest "notmuch-show: don't elide non-matching messages"
+test_emacs '(let ((notmuch-show-only-matching-messages nil))
+       (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-elide-non-matching-messages-off
+
+test_begin_subtest "notmuch-show: elide non-matching messages"
+test_emacs '(let ((notmuch-show-only-matching-messages t))
+       (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-elide-non-matching-messages-on
+
+test_begin_subtest "notmuch-show: elide non-matching messages (w/ notmuch-show-toggle-elide-non-matching)"
+test_emacs '(let ((notmuch-show-only-matching-messages nil))
+       (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (notmuch-show-toggle-elide-non-matching)
+       (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-elide-non-matching-messages-on
+
+test_begin_subtest "notmuch-show: elide non-matching messages (w/ prefix arg to notmuch-show)"
+test_emacs '(let ((notmuch-show-only-matching-messages nil))
+       (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (let ((current-prefix-arg t))
+         (notmuch-search-show-thread))
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-elide-non-matching-messages-on
+
+test_begin_subtest "notmuch-show: disable indentation of thread content (w/ notmuch-show-toggle-thread-indentation)"
+test_emacs '(notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (notmuch-show-toggle-thread-indentation)
+       (test-visible-output)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-indent-thread-content-off
+
+test_begin_subtest "id buttonization"
+add_message '[body]="
+id:abc
+id:abc.def. id:abc,def, id:abc;def; id:abc:def:
+id:foo@bar.?baz? id:foo@bar!.baz!
+(id:foo@bar.baz) [id:foo@bar.baz]
+id:foo@bar.baz...
+id:2+2=5
+id:=_-:/.[]@$%+
+id:abc)def
+id:ab\"c def
+id:\"abc\"def
+id:\"ab\"\"c\"def
+id:\"ab c\"def
+id:\"abc\".def
+id:\"abc
+\"
+id:)
+id:
+cid:xxx
+mid:abc mid:abc/def
+mid:abc%20def
+mid:abc. mid:abc, mid:abc;"'
+test_emacs '(notmuch-show "id:'$gen_msg_id'")
+       (notmuch-test-mark-links)
+       (test-visible-output)'
+cat <<EOF >EXPECTED
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox)
+Subject: id buttonization
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+
+<<id:abc>>
+<<id:abc.def>>. <<id:abc,def>>, <<id:abc;def>>; <<id:abc:def>>:
+<<id:foo@bar.?baz>>? <<id:foo@bar!.baz>>!
+(<<id:foo@bar.baz>>) [<<id:foo@bar.baz>>]
+<<id:foo@bar.baz>>...
+<<id:2+2=5>>
+<<id:=_-:/.[]@$%+>>
+<<id:abc>>)def
+<<id:ab"c>> def
+<<id:"abc">>def
+<<id:"ab""c">>def
+<<id:"ab c">>def
+<<id:"abc">>.def
+id:"abc
+"
+id:)
+id:
+cid:xxx
+<<mid:abc>> <<mid:abc/def>>
+<<mid:abc%20def>>
+<<mid:abc>>. <<mid:abc>>, <<mid:abc>>;
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+
+test_begin_subtest "Show handles subprocess errors"
+cat > notmuch_fail <<EOF
+#!/bin/sh
+echo This is output
+echo This is an error >&2
+exit 1
+EOF
+chmod a+x notmuch_fail
+test_emacs "(let ((notmuch-command \"$PWD/notmuch_fail\"))
+              (with-current-buffer \"*Messages*\" (erase-buffer))
+              (condition-case err
+                  (notmuch-show \"*\")
+                (error (message \"%s\" (second err))))
+              (notmuch-test-wait)
+              (with-current-buffer \"*Messages*\"
+                 (test-output \"MESSAGES\"))
+              (with-current-buffer \"*Notmuch errors*\"
+                 (test-output \"ERROR\"))
+              (test-output))"
+sed -i -e 's/^\[.*\]$/[XXX]/' ERROR
+test_expect_equal "$(cat OUTPUT; echo ---; cat MESSAGES; echo ---; cat ERROR)" "\
+---
+This is an error (see *Notmuch errors* for more details)
+---
+[XXX]
+This is an error
+command: $PWD/notmuch_fail show --format\\=json --format-version\\=1 --exclude\\=false \\' \\* \\'
+exit status: 1
+stderr:
+This is an error
+stdout:
+This is output"
+
+
 test_done
diff --git a/test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-off b/test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-off
new file mode 100644 (file)
index 0000000..b31fe62
--- /dev/null
@@ -0,0 +1,79 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+Subject: [notmuch] Working with Maildir storage?
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 14:00:54 -0500
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ text/plain ]
+I saw the LWN article and decided to take a look at notmuch.  I'm
+currently using mutt and mairix to index and read a collection of
+Maildir mail folders (around 40,000 messages total).
+
+notmuch indexed the messages without complaint, but my attempt at
+searching bombed out. Running, for example:
+
+  notmuch search storage
+
+Resulted in 4604 lines of errors along the lines of:
+
+  Error opening
+  /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+  Too many open files
+
+I'm curious if this is expected behavior (i.e., notmuch does not work
+with Maildir) or if something else is going on.
+
+Cheers,
+
+[ 4-line signature. Click/Enter to show. ]
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed ]
+  [ multipart/signed ]
+  [ text/plain ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 4-line signature. Click/Enter to show. ]
+  [ application/pgp-signature ]
+  [ text/plain ]
+  [ 4-line signature. Click/Enter to show. ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed ]
+    [ multipart/signed ]
+    [ text/plain ]
+    > I've also pushed a slightly more complicated (and complete) fix to my
+    > private notmuch repository
+
+    The version of lib/messages.cc in your repo doesn't build because it's
+    missing "#include <stdint.h>" (for the uint32_t on line 466).
+
+    [ 4-line signature. Click/Enter to show. ]
+    [ application/pgp-signature ]
+    [ text/plain ]
+    [ 4-line signature. Click/Enter to show. ]
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on b/test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on
new file mode 100644 (file)
index 0000000..bafb479
--- /dev/null
@@ -0,0 +1,75 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+Subject: [notmuch] Working with Maildir storage?
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 14:00:54 -0500
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ text/plain ]
+I saw the LWN article and decided to take a look at notmuch.  I'm
+currently using mutt and mairix to index and read a collection of
+Maildir mail folders (around 40,000 messages total).
+
+notmuch indexed the messages without complaint, but my attempt at
+searching bombed out. Running, for example:
+
+  notmuch search storage
+
+Resulted in 4604 lines of errors along the lines of:
+
+  Error opening
+  /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+  Too many open files
+
+I'm curious if this is expected behavior (i.e., notmuch does not work
+with Maildir) or if something else is going on.
+
+Cheers,
+
+[ 4-line signature. Click/Enter to show. ]
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed ]
+  [ multipart/signed ]
+  [ text/plain ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 4-line signature. Click/Enter to show. ]
+  [ application/pgp-signature ]
+  [ text/plain ]
+  [ 4-line signature. Click/Enter to show. ]
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed ]
+    [ multipart/signed ]
+    [ text/plain ]
+    > I've also pushed a slightly more complicated (and complete) fix to my
+    > private notmuch repository
+
+    The version of lib/messages.cc in your repo doesn't build because it's
+    missing "#include <stdint.h>" (for the uint32_t on line 466).
+
+    [ 4-line signature. Click/Enter to show. ]
+    [ application/pgp-signature ]
+    [ text/plain ]
+    [ 4-line signature. Click/Enter to show. ]
diff --git a/test/emacs-show.expected-output/notmuch-show-indent-thread-content-off b/test/emacs-show.expected-output/notmuch-show-indent-thread-content-off
new file mode 100644 (file)
index 0000000..37b4f7d
--- /dev/null
@@ -0,0 +1,79 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+Subject: [notmuch] Working with Maildir storage?
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 14:00:54 -0500
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ text/plain ]
+I saw the LWN article and decided to take a look at notmuch.  I'm
+currently using mutt and mairix to index and read a collection of
+Maildir mail folders (around 40,000 messages total).
+
+notmuch indexed the messages without complaint, but my attempt at
+searching bombed out. Running, for example:
+
+  notmuch search storage
+
+Resulted in 4604 lines of errors along the lines of:
+
+  Error opening
+  /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+  Too many open files
+
+I'm curious if this is expected behavior (i.e., notmuch does not work
+with Maildir) or if something else is going on.
+
+Cheers,
+
+[ 4-line signature. Click/Enter to show. ]
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+Subject: Re: [notmuch] Working with Maildir storage?
+To: Mikhail Gusarov <dottedmag@dottedmag.net>
+Cc: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ text/plain ]
+> See the patch just posted here.
+
+Is the list archived anywhere?  The obvious archives
+(http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+think I subscribed too late to get the patch (I only just saw the
+discussion about it).
+
+It doesn't look like the patch is in git yet.
+
+-- Lars
+
+[ 4-line signature. Click/Enter to show. ]
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+Subject: Re: [notmuch] Working with Maildir storage?
+To: Keith Packard <keithp@keithp.com>
+Cc: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ text/plain ]
+> I've also pushed a slightly more complicated (and complete) fix to my
+> private notmuch repository
+
+The version of lib/messages.cc in your repo doesn't build because it's
+missing "#include <stdint.h>" (for the uint32_t on line 466).
+
+[ 4-line signature. Click/Enter to show. ]
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-off b/test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-off
new file mode 100644 (file)
index 0000000..3282c7b
--- /dev/null
@@ -0,0 +1,31 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed unread)
+Subject: [notmuch] Working with Maildir storage?
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed ]
+  [ multipart/signed ]
+  [ text/plain ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 4-line signature. Click/Enter to show. ]
+  [ application/pgp-signature ]
+  [ text/plain ]
+  [ 4-line signature. Click/Enter to show. ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-on b/test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-on
new file mode 100644 (file)
index 0000000..eaa557a
--- /dev/null
@@ -0,0 +1,32 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed unread)
+Subject: [notmuch] Working with Maildir storage?
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed ]
+  [ multipart/signed ]
+  [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+  [ text/plain ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 4-line signature. Click/Enter to show. ]
+  [ application/pgp-signature ]
+  [ text/plain ]
+  [ 4-line signature. Click/Enter to show. ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
index 176e6859b0dbaba87d13f9bf87121eb49e469a83..230c324d2ea4d60aa35bfbd4af30eb73a6c716d1 100755 (executable)
@@ -1,17 +1,17 @@
 #!/usr/bin/env bash
 
 test_description="emacs: mail subject to filename"
-. test-lib.sh
+. ./test-lib.sh
 
 # emacs server can't be started in a child process with $(test_emacs ...)
-test_emacs '(ignore)'
+test_emacs '(ignore)' > /dev/null
 
 # test notmuch-wash-subject-to-patch-sequence-number (subject)
 test_begin_subtest "no patch sequence number"
-output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
-      "[PATCH] A normal patch subject without numbers")'
+output=$(test_emacs '(format "%S" (notmuch-wash-subject-to-patch-sequence-number
+      "[PATCH] A normal patch subject without numbers"))'
 )
-test_expect_equal "$output" ""
+test_expect_equal "$output" '"nil"'
 
 test_begin_subtest "patch sequence number #1"
 output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
@@ -60,55 +60,55 @@ test_begin_subtest "filename #1"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "just a subject line")'
 )
-test_expect_equal $output '"just-a-subject-line"'
+test_expect_equal "$output" '"just-a-subject-line"'
 
 test_begin_subtest "filename #2"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       " [any]  [prefixes are ] [removed!] from the subject")'
 )
-test_expect_equal $output '"from-the-subject"'
+test_expect_equal "$output" '"from-the-subject"'
 
 test_begin_subtest "filename #3"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "  leading and trailing space  ")'
 )
-test_expect_equal $output '"leading-and-trailing-space"'
+test_expect_equal "$output" '"leading-and-trailing-space"'
 
 test_begin_subtest "filename #4"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "!#  leading ()// &%, and in between_and_trailing garbage ()(&%%")'
 )
-test_expect_equal $output '"-leading-and-in-between_and_trailing-garbage"'
+test_expect_equal "$output" '"-leading-and-in-between_and_trailing-garbage"'
 
 test_begin_subtest "filename #5"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_01234567890")'
 )
-test_expect_equal $output '"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_01234567890"'
+test_expect_equal "$output" '"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_01234567890"'
 
 test_begin_subtest "filename #6"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "sequences of ... are squashed and trailing are removed ...")'
 )
-test_expect_equal $output '"sequences-of-.-are-squashed-and-trailing-are-removed"'
+test_expect_equal "$output" '"sequences-of-.-are-squashed-and-trailing-are-removed"'
 
 test_begin_subtest "filename #7"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "max length test" 1)'
 )
-test_expect_equal $output '"m"'
+test_expect_equal "$output" '"m"'
 
 test_begin_subtest "filename #8"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "max length test /&(/%&/%%&¤%¤" 20)'
 )
-test_expect_equal $output '"max-length-test"'
+test_expect_equal "$output" '"max-length-test"'
 
 test_begin_subtest "filename #9"
 output=$(test_emacs '(notmuch-wash-subject-to-filename
       "[a prefix] [is only separated] by [spaces], so \"by\" is not okay!")'
 )
-test_expect_equal $output '"by-spaces-so-by-is-not-okay"'
+test_expect_equal "$output" '"by-spaces-so-by-is-not-okay"'
 
 # test notmuch-wash-subject-to-patch-filename (subject)
 test_begin_subtest "patch filename #1"
index 0e1f9fc710e934a46eeabe2da98ddb716315ff49..ca4a7988567452af1f2d3296ba462d275516fb3f 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 test_description="emacs test function sanity"
-. test-lib.sh
+. ./test-lib.sh
 
 test_begin_subtest "emacs test function sanity"
 test_emacs_expect_t 't'
diff --git a/test/emacs.expected-output/notmuch-show-message-with-headers-hidden b/test/emacs.expected-output/notmuch-show-message-with-headers-hidden
new file mode 100644 (file)
index 0000000..9d7f91b
--- /dev/null
@@ -0,0 +1,22 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox unread)
+Subject: [notmuch] What a great idea!
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+
+ On Tue, Nov 17, 2009 at 11:35 PM, Jan Janak <jan at ryngle.com> wrote:
+ > Hello,
+ >
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ [ 2 more citation lines. Click/Enter to show. ]
+ >
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://org-mode.org
+
+ Sorry, wrong URL, the correct one is: http://orgmode.org
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+   -- Jan
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs.expected-output/notmuch-show-message-with-headers-visible b/test/emacs.expected-output/notmuch-show-message-with-headers-visible
new file mode 100644 (file)
index 0000000..8efbd60
--- /dev/null
@@ -0,0 +1,25 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox unread)
+Subject: [notmuch] What a great idea!
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+ Subject: [notmuch] What a great idea!
+ To: notmuch@notmuchmail.org
+ Date: Tue, 17 Nov 2009 23:38:47 +0100
+
+ On Tue, Nov 17, 2009 at 11:35 PM, Jan Janak <jan at ryngle.com> wrote:
+ > Hello,
+ >
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ [ 2 more citation lines. Click/Enter to show. ]
+ >
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://org-mode.org
+
+ Sorry, wrong URL, the correct one is: http://orgmode.org
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+   -- Jan
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs.expected-output/notmuch-show-thread-with-all-messages-collapsed b/test/emacs.expected-output/notmuch-show-thread-with-all-messages-collapsed
new file mode 100644 (file)
index 0000000..73b0e60
--- /dev/null
@@ -0,0 +1,4 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+Subject: [notmuch] What a great idea!
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs.expected-output/notmuch-show-thread-with-all-messages-uncollapsed b/test/emacs.expected-output/notmuch-show-thread-with-all-messages-uncollapsed
new file mode 100644 (file)
index 0000000..bd5598e
--- /dev/null
@@ -0,0 +1,79 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+Subject: [notmuch] What a great idea!
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 23:35:30 +0100
+
+Hello,
+
+First of all, notmuch is a wonderful idea, both the cmdline tool and
+the emacs interface! Thanks a lot for writing it, I was really excited
+when I read the announcement today.
+
+Have you considered sending an announcement to the org-mode mailing list?
+http://org-mode.org
+
+Various ways of searching/referencing emails from emacs were discussed
+there several times and none of them were as elegant as notmuch (not
+even close). Maybe notmuch would attract some of the developers
+there..
+
+   -- Jan
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+ Subject: [notmuch] What a great idea!
+ To: notmuch@notmuchmail.org
+ Date: Tue, 17 Nov 2009 23:38:47 +0100
+
+ On Tue, Nov 17, 2009 at 11:35 PM, Jan Janak <jan at ryngle.com> wrote:
+ > Hello,
+ >
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ [ 2 more citation lines. Click/Enter to show. ]
+ >
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://org-mode.org
+
+ Sorry, wrong URL, the correct one is: http://orgmode.org
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+   -- Jan
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
+ Subject: [notmuch] What a great idea!
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 02:49:52 -0800
+
+ On Tue, 17 Nov 2009 23:35:30 +0100, Jan Janak <jan at ryngle.com> wrote:
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ > the emacs interface! Thanks a lot for writing it, I was really excited
+ > when I read the announcement today.
+
+ Ah, here's where I planned a nice welcome. So welcome (again), Jan! :-)
+
+ I've been having a lot of fun with notmuch already, (though there have
+ been some days of pain before it was functional enough and my
+ email-reply latency went way up). But regardless---I got through that,
+ and I'm able to work more efficiently with notmuch now than I could with
+ sup before. So I'm happy.
+
+ And I'm delighted when other people find this interesting as well.
+
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://orgmode.org
+
+ Thanks for the idea. I think I may have looked into org-mode years ago,
+ (when I was investigating planner-mode and various emacs "personal wiki"
+ systems for keeping random notes and what-not).
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+ Yeah. I'll drop them a mail. Having a real emacs wizard on board would
+ be nice. (I'm afraid the elisp I've written so far for this project is
+ fairly grim.)
+
+ -Carl
index 9f4b9c79de47860f4c128074f92ffeda43310347..f7df725eea611797c333ba7a5a759cf31b9171f0 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 test_description="online help"
-. test-lib.sh
+. ./test-lib.sh
 
 test_expect_success 'notmuch --help' 'notmuch --help'
 test_expect_success 'notmuch --help tag' 'notmuch --help tag'
diff --git a/test/hex-escaping b/test/hex-escaping
new file mode 100755 (executable)
index 0000000..ad50e1b
--- /dev/null
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+test_description="hex encoding and decoding"
+. ./test-lib.sh
+
+test_begin_subtest "round trip"
+find $TEST_DIRECTORY/corpus -type f -print | sort | xargs cat > EXPECTED
+$TEST_DIRECTORY/hex-xcode --direction=encode < EXPECTED | $TEST_DIRECTORY/hex-xcode --direction=decode > OUTPUT
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "punctuation"
+tag1='comic_swear=$&^%$^%\\//-+$^%$'
+tag_enc1=$($TEST_DIRECTORY/hex-xcode --direction=encode "$tag1")
+test_expect_equal "$tag_enc1" "comic_swear=%24%26%5e%25%24%5e%25%5c%5c%2f%2f-+%24%5e%25%24"
+
+test_begin_subtest "round trip newlines"
+printf 'this\n tag\t has\n spaces\n' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --direction=encode  < EXPECTED.$test_count |\
+       $TEST_DIRECTORY/hex-xcode --direction=decode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "round trip 8bit chars"
+echo '%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --direction=decode  < EXPECTED.$test_count |\
+    $TEST_DIRECTORY/hex-xcode --direction=encode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "round trip (in-place)"
+find $TEST_DIRECTORY/corpus -type f -print | sort | xargs cat > EXPECTED
+$TEST_DIRECTORY/hex-xcode --in-place --direction=encode < EXPECTED |\
+     $TEST_DIRECTORY/hex-xcode --in-place --direction=decode > OUTPUT
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "punctuation (in-place)"
+tag1='comic_swear=$&^%$^%\\//-+$^%$'
+tag_enc1=$($TEST_DIRECTORY/hex-xcode --in-place --direction=encode "$tag1")
+test_expect_equal "$tag_enc1" "comic_swear=%24%26%5e%25%24%5e%25%5c%5c%2f%2f-+%24%5e%25%24"
+
+test_begin_subtest "round trip newlines (in-place)"
+printf 'this\n tag\t has\n spaces\n' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --in-place --direction=encode  < EXPECTED.$test_count |\
+    $TEST_DIRECTORY/hex-xcode --in-place --direction=decode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "round trip 8bit chars (in-place)"
+echo '%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --in-place --direction=decode  < EXPECTED.$test_count |\
+    $TEST_DIRECTORY/hex-xcode --in-place --direction=encode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_done
diff --git a/test/hex-xcode.c b/test/hex-xcode.c
new file mode 100644 (file)
index 0000000..65d4956
--- /dev/null
@@ -0,0 +1,109 @@
+/* No, nothing to to with IDE from Apple Inc.
+ * testbed for ../util/hex-escape.c.
+ *
+ * usage:
+ * hex-xcode [--direction=(encode|decode)] [--omit-newline] < file
+ * hex-xcode [--direction=(encode|decode)] [--omit-newline] [--in-place] arg1 arg2 arg3 ...
+ *
+ */
+
+#include "notmuch-client.h"
+#include "hex-escape.h"
+#include <assert.h>
+
+enum direction {
+    ENCODE,
+    DECODE
+};
+
+static int inplace = FALSE;
+
+static int
+xcode (void *ctx, enum direction dir, char *in, char **buf_p, size_t *size_p)
+{
+    hex_status_t status;
+
+    if (dir == ENCODE)
+       status = hex_encode (ctx, in, buf_p, size_p);
+    else
+       if (inplace) {
+           status = hex_decode_inplace (in);
+           *buf_p = in;
+           *size_p = strlen(in);
+       } else {
+           status = hex_decode (ctx, in, buf_p, size_p);
+       }
+
+    if (status == HEX_SUCCESS)
+       fputs (*buf_p, stdout);
+
+    return status;
+}
+
+int
+main (int argc, char **argv)
+{
+
+    enum direction dir = DECODE;
+    int omit_newline = FALSE;
+
+    notmuch_opt_desc_t options[] = {
+       { NOTMUCH_OPT_KEYWORD, &dir, "direction", 'd',
+         (notmuch_keyword_t []){ { "encode", ENCODE },
+                                 { "decode", DECODE },
+                                 { 0, 0 } } },
+       { NOTMUCH_OPT_BOOLEAN, &omit_newline, "omit-newline", 'n', 0 },
+       { NOTMUCH_OPT_BOOLEAN, &inplace, "in-place", 'i', 0 },
+       { 0, 0, 0, 0, 0 }
+    };
+
+    int opt_index = parse_arguments (argc, argv, options, 1);
+
+    if (opt_index < 0)
+       exit (1);
+
+    void *ctx = talloc_new (NULL);
+
+    char *line = NULL;
+    size_t line_size;
+    ssize_t line_len;
+
+    char *buffer = NULL;
+    size_t buf_size = 0;
+
+    notmuch_bool_t read_stdin = TRUE;
+
+    for (; opt_index < argc; opt_index++) {
+
+       if (xcode (ctx, dir, argv[opt_index],
+                  &buffer, &buf_size) != HEX_SUCCESS)
+           return 1;
+
+       if (! omit_newline)
+           putchar ('\n');
+
+       read_stdin = FALSE;
+    }
+
+    if (! read_stdin)
+       return 0;
+
+    while ((line_len = getline (&line, &line_size, stdin)) != -1) {
+
+       chomp_newline (line);
+
+       if (xcode (ctx, dir, line, &buffer, &buf_size) != HEX_SUCCESS)
+           return 1;
+
+       if (! omit_newline)
+           putchar ('\n');
+
+    }
+
+    if (line)
+       free (line);
+
+    talloc_free (ctx);
+
+    return 0;
+}
index 643978869d25b95ef5a4ea6d23f1fc493de9f09c..b87b7f6d7165d5854792f4971938bd01e00648f8 100755 (executable)
--- a/test/json
+++ b/test/json
@@ -3,26 +3,36 @@ test_description="--format=json output"
 . ./test-lib.sh
 
 test_begin_subtest "Show message: json"
-add_message "[subject]=\"json-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-show-message\""
+add_message "[subject]=\"json-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"json-show-message\""
 output=$(notmuch show --format=json "json-show-message")
-test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
+test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
+
+# This should be the same output as above.
+test_begin_subtest "Show message: json --body=true"
+output=$(notmuch show --format=json --body=true "json-show-message")
+test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
+
+test_begin_subtest "Show message: json --body=false"
+output=$(notmuch show --format=json --body=false "json-show-message")
+test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}}, []]]]"
 
 test_begin_subtest "Search message: json"
 add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
 output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)
-test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-subject\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-subject\",
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
 
 test_begin_subtest "Show message: json, utf-8"
 add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
 output=$(notmuch show --format=json "jsön-show-méssage")
-test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"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>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]"
+test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"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>\", \"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'
@@ -35,18 +45,27 @@ emacs_deliver_message \
      (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, \"excluded\": false, \"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\", \"Date\": \"Sat, 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\"}]}]}, []]]]"
+# Get length of README after base64-encoding, minus additional newline.
+attachment_length=$(( $(base64 $TEST_DIRECTORY/README | wc -c) - 1 ))
+test_expect_equal_json "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\": false, \"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\", \"Date\": \"Sat, 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\", \"content-length\": $attachment_length, \"content-transfer-encoding\": \"base64\", \"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)
-test_expect_equal "$output" "[{\"thread\": \"XXX\",
-\"timestamp\": 946728000,
-\"date_relative\": \"2000-01-01\",
-\"matched\": 1,
-\"total\": 1,
-\"authors\": \"Notmuch Test Suite\",
-\"subject\": \"json-search-utf8-body-sübjéct\",
-\"tags\": [\"inbox\", \"unread\"]}]"
+test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-utf8-body-sübjéct\",
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
+
+test_expect_code 20 "Format version: too low" \
+    "notmuch search --format-version=0 \\*"
+
+test_expect_code 21 "Format version: too high" \
+    "notmuch search --format-version=999 \\*"
 
 test_done
index d72ec07e9cb9eaaeaf08f8f2cdbbbb0df750e336..0fc742a419bac7aacb69a762399bda4b11c745fd 100755 (executable)
@@ -4,12 +4,9 @@ test_description="maildir synchronization"
 
 . ./test-lib.sh
 
-# Much easier to examine differences if the "notmuch show
-# --format=json" output includes some newlines. Also, need to avoid
-# including the local value of MAIL_DIR in the result.
+# Avoid including the local value of MAIL_DIR in the result.
 filter_show_json() {
-    sed -e 's/, /,\n/g'  | sed -e "s|${MAIL_DIR}/|MAIL_DIR/|"
-    echo
+    sed -e "s|${MAIL_DIR}/|MAIL_DIR/|"
 }
 
 # Create the expected maildir structure
@@ -44,7 +41,7 @@ test_expect_equal "$output" "adding-replied-tag:2,RS"
 
 test_begin_subtest "notmuch show works with renamed file (without notmuch new)"
 output=$(notmuch show --format=json id:${gen_msg_id} | filter_show_json)
-test_expect_equal "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
+test_expect_equal_json "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
 "match": true,
 "excluded": false,
 "filename": "MAIL_DIR/cur/adding-replied-tag:2,RS",
@@ -54,8 +51,7 @@ test_expect_equal "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
 "headers": {"Subject": "Adding replied tag",
 "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
 "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
-"Date": "Fri,
-05 Jan 2001 15:43:57 +0000"},
+"Date": "Fri, 05 Jan 2001 15:43:57 +0000"},
 "body": [{"id": 1,
 "content-type": "text/plain",
 "content": "This is just a test message (#3)\n"}]},
@@ -128,9 +124,9 @@ 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
-notmuch dump dump.txt
+notmuch dump --output=dump.txt
 NOTMUCH_NEW >/dev/null
-notmuch restore dump.txt
+notmuch restore --input=dump.txt
 output=$(ls $MAIL_DIR/cur)
 test_expect_equal "$output" "$expected"
 
@@ -166,4 +162,13 @@ add_message [subject]='"Non-compliant maildir info"' [dir]=cur [filename]='non-c
 notmuch tag +unread +draft -flagged subject:"Non-compliant maildir info"
 test_expect_equal "$(cd $MAIL_DIR/cur/; ls non-compliant*)" "non-compliant-maildir-info:2,These-are-not-flags-in-ASCII-order-donottouch"
 
+test_begin_subtest "Files in new/ get default synchronized tags"
+OLDCONFIG=$(notmuch config get new.tags)
+notmuch config set new.tags test
+add_message [subject]='"File in new/"' [dir]=new [filename]='file-in-new'
+notmuch config set new.tags $OLDCONFIG
+notmuch search 'subject:"File in new"' | notmuch_search_sanitize > output
+test_expect_equal "$(< output)" \
+"thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; File in new/ (test unread)"
+
 test_done
diff --git a/test/missing-headers b/test/missing-headers
new file mode 100755 (executable)
index 0000000..f14b878
--- /dev/null
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+test_description='messages with missing headers'
+. ./test-lib.sh
+
+# Notmuch requires at least one of from, subject, or to or it will
+# ignore the file.  Generate two messages so that together they cover
+# all possible missing headers.  We also give one of the messages a
+# date to ensure stable result ordering.
+
+cat <<EOF > "${MAIL_DIR}/msg-2"
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+
+Body
+EOF
+
+cat <<EOF > "${MAIL_DIR}/msg-1"
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+
+Body
+EOF
+
+NOTMUCH_NEW
+
+test_begin_subtest "Search: text"
+output=$(notmuch search '*' | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] (null);  (inbox unread)
+thread:XXX   1970-01-01 [1/1] Notmuch Test Suite;  (inbox unread)"
+
+test_begin_subtest "Search: json"
+output=$(notmuch search --format=json '*' | notmuch_search_sanitize)
+test_expect_equal_json "$output" '
+[
+    {
+        "authors": "",
+        "date_relative": "2001-01-05",
+        "matched": 1,
+        "subject": "",
+        "tags": [
+            "inbox",
+            "unread"
+        ],
+        "thread": "XXX",
+        "timestamp": 978709437,
+        "total": 1
+    },
+    {
+        "authors": "Notmuch Test Suite",
+        "date_relative": "1970-01-01",
+        "matched": 1,
+        "subject": "",
+        "tags": [
+            "inbox",
+            "unread"
+        ],
+        "thread": "XXX",
+        "timestamp": 0,
+        "total": 1
+    }
+]'
+
+test_begin_subtest "Show: text"
+output=$(notmuch show '*' | notmuch_show_sanitize)
+test_expect_equal "$output" "\
+\fmessage{ id:notmuch-sha1-7a6e4eac383ef958fcd3ebf2143db71b8ff01161 depth:0 match:1 excluded:0 filename:/XXX/mail/msg-2
+\fheader{
+ (2001-01-05) (inbox unread)
+Subject: (null)
+From: (null)
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+Body
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:notmuch-sha1-ca55943aff7a72baf2ab21fa74fab3d632401334 depth:0 match:1 excluded:0 filename:/XXX/mail/msg-1
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (1970-01-01) (inbox unread)
+Subject: (null)
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Thu, 01 Jan 1970 00:00:00 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+Body
+\fpart}
+\fbody}
+\fmessage}"
+
+test_begin_subtest "Show: json"
+output=$(notmuch show --format=json '*' | notmuch_json_show_sanitize)
+test_expect_equal_json "$output" '
+[
+    [
+        [
+            {
+                "body": [
+                    {
+                        "content": "Body\n",
+                        "content-type": "text/plain",
+                        "id": 1
+                    }
+                ],
+                "date_relative": "2001-01-05",
+                "excluded": false,
+                "filename": "YYYYY",
+                "headers": {
+                    "Date": "Fri, 05 Jan 2001 15:43:57 +0000",
+                    "From": "",
+                    "Subject": "",
+                    "To": "Notmuch Test Suite <test_suite@notmuchmail.org>"
+                },
+                "id": "XXXXX",
+                "match": true,
+                "tags": [
+                    "inbox",
+                    "unread"
+                ],
+                "timestamp": 978709437
+            },
+            []
+        ]
+    ],
+    [
+        [
+            {
+                "body": [
+                    {
+                        "content": "Body\n",
+                        "content-type": "text/plain",
+                        "id": 1
+                    }
+                ],
+                "date_relative": "1970-01-01",
+                "excluded": false,
+                "filename": "YYYYY",
+                "headers": {
+                    "Date": "Thu, 01 Jan 1970 00:00:00 +0000",
+                    "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+                    "Subject": ""
+                },
+                "id": "XXXXX",
+                "match": true,
+                "tags": [
+                    "inbox",
+                    "unread"
+                ],
+                "timestamp": 0
+            },
+            []
+        ]
+    ]
+]'
+
+
+test_done
index 72d3927651a1e12f6ada517f6936ab56b59ebe16..c974226efd773ba46fd5be9f900919d3aa29fbcc 100755 (executable)
@@ -319,122 +319,102 @@ 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"
-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
+notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-
 {"id": "87liy5ap00.fsf@yoom.home.cworth.org", "match": true, "excluded": false, "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", "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": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
-{"id": 5, "content-type": "text/html"}, 
+{"id": 5, "content-type": "text/html", "content-length": 71},
 {"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"}]}]}
+{"id": 9, "content-type": "application/pgp-signature", "content-length": 197}]}]}
 EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 test_begin_subtest "--format=json --part=1, message body"
-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
+notmuch show --format=json --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >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": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
-{"id": 5, "content-type": "text/html"}, 
+{"id": 5, "content-type": "text/html", "content-length": 71},
 {"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"}]}
+{"id": 9, "content-type": "application/pgp-signature", "content-length": 197}]}
 EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 test_begin_subtest "--format=json --part=2, multipart/mixed"
-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
+notmuch show --format=json --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-
 {"id": 2, "content-type": "multipart/mixed", "content": [
 {"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
-{"id": 5, "content-type": "text/html"}, 
+{"id": 5, "content-type": "text/html", "content-length": 71},
 {"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_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 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
+notmuch show --format=json --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-
 {"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
-{"id": 5, "content-type": "text/html"}, 
+{"id": 5, "content-type": "text/html", "content-length": 71},
 {"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_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 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
+notmuch show --format=json --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-
 {"id": 4, "content-type": "multipart/alternative", "content": [
-{"id": 5, "content-type": "text/html"}, 
+{"id": 5, "content-type": "text/html", "content-length": 71},
 {"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_expect_equal_json "$(cat OUTPUT)" "$(cat 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
+notmuch show --format=json --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-
-{"id": 5, "content-type": "text/html"}
+{"id": 5, "content-type": "text/html", "content-length": 71}
 EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 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
+notmuch show --format=json --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' >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_expect_equal_json "$(cat OUTPUT)" "$(cat 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
+notmuch show --format=json --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >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_expect_equal_json "$(cat OUTPUT)" "$(cat 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
+notmuch show --format=json --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org' >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_expect_equal_json "$(cat OUTPUT)" "$(cat 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
+notmuch show --format=json --part=9 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-
-{"id": 9, "content-type": "application/pgp-signature"}
+{"id": 9, "content-type": "application/pgp-signature", "content-length": 197}
 EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 test_expect_success \
     "--format=json --part=10, no part, expect error" \
@@ -617,8 +597,7 @@ notmuch reply --format=json 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_j
 cat <<EOF >EXPECTED
 {"reply-headers": {"Subject": "Re: Multipart message",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
- "To": "Carl Worth <cworth@cworth.org>,
- cworth@cworth.org",
+ "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>"},
  "original": {"id": "XXXXX",
@@ -631,8 +610,7 @@ cat <<EOF >EXPECTED
  "headers": {"Subject": "Multipart message",
  "From": "Carl Worth <cworth@cworth.org>",
  "To": "cworth@cworth.org",
- "Date": "Fri,
- 05 Jan 2001 15:43:57 +0000"},
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"},
  "body": [{"id": 1,
  "content-type": "multipart/signed",
  "content": [{"id": 2,
@@ -642,27 +620,27 @@ cat <<EOF >EXPECTED
  "content": [{"headers": {"Subject": "html message",
  "From": "Carl Worth <cworth@cworth.org>",
  "To": "cworth@cworth.org",
- "Date": "Fri,
- 05 Jan 2001 15:42:57 +0000"},
+ "Date": "Fri, 05 Jan 2001 15:42:57 +0000"},
  "body": [{"id": 4,
  "content-type": "multipart/alternative",
  "content": [{"id": 5,
- "content-type": "text/html"},
+ "content-type": "text/html",
+ "content-length": 71},
  {"id": 6,
  "content-type": "text/plain",
- "content": "This is an embedded message,
- with a multipart/alternative part.\n"}]}]}]},
+ "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]},
  {"id": 7,
  "content-type": "text/plain",
- "filename": "YYYYY",
+ "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"}]}]}}
+ "content-type": "application/pgp-signature",
+ "content-length": 197}]}]}}
 EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 test_begin_subtest "'notmuch show --part' does not corrupt a part with CRLF pair"
 notmuch show --format=raw --part=3 id:base64-part-with-crlf > crlf.out
index 99f9913e17d83ccc3a38dae234494c32cdd8ee40..3eff2fe9e22ec8d25f0e85c523bcb914a519bdb9 100755 (executable)
--- a/test/new
+++ b/test/new
@@ -136,6 +136,16 @@ output=$(NOTMUCH_NEW)
 test_expect_equal "$output" "Added 1 new message to the database."
 
 
+test_begin_subtest "Broken symlink aborts"
+ln -s does-not-exist "${MAIL_DIR}/broken"
+output=$(NOTMUCH_NEW 2>&1)
+test_expect_equal "$output" \
+"Error reading file ${MAIL_DIR}/broken: No such file or directory
+Note: A fatal error was encountered: Something went wrong trying to read or write a file
+No new mail."
+rm "${MAIL_DIR}/broken"
+
+
 test_begin_subtest "New two-level directory"
 
 generate_message [dir]=two/levels
@@ -153,18 +163,51 @@ rm -rf "${MAIL_DIR}"/two
 output=$(NOTMUCH_NEW)
 test_expect_equal "$output" "No new mail. Removed 3 messages."
 
+test_begin_subtest "Support single-message mbox (deprecated)"
+cat > "${MAIL_DIR}"/mbox_file1 <<EOF
+From test_suite@notmuchmail.org Fri Jan  5 15:43:57 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Test mbox message 1
+
+Body.
+EOF
+output=$(NOTMUCH_NEW 2>&1)
+test_expect_equal "$output" \
+"Warning: ${MAIL_DIR}/mbox_file1 is an mbox containing a single message,
+likely caused by misconfigured mail delivery.  Support for single-message
+mboxes is deprecated and may be removed in the future.
+Added 1 new message to the database."
+
 # This test requires that notmuch new has been run at least once.
 test_begin_subtest "Skip and report non-mail files"
 generate_message
 mkdir -p "${MAIL_DIR}"/.git && touch "${MAIL_DIR}"/.git/config
 touch "${MAIL_DIR}"/ignored_file
 touch "${MAIL_DIR}"/.ignored_hidden_file
+cat > "${MAIL_DIR}"/mbox_file <<EOF
+From test_suite@notmuchmail.org Fri Jan  5 15:43:57 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Test mbox message 1
+
+Body.
+
+From test_suite@notmuchmail.org Fri Jan  5 15:43:57 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Test mbox message 2
+
+Body 2.
+EOF
 output=$(NOTMUCH_NEW 2>&1)
 test_expect_equal "$output" \
 "Note: Ignoring non-mail file: ${MAIL_DIR}/.git/config
 Note: Ignoring non-mail file: ${MAIL_DIR}/.ignored_hidden_file
 Note: Ignoring non-mail file: ${MAIL_DIR}/ignored_file
+Note: Ignoring non-mail file: ${MAIL_DIR}/mbox_file
 Added 1 new message to the database."
+rm "${MAIL_DIR}"/mbox_file
 
 test_begin_subtest "Ignore files and directories specified in new.ignore"
 generate_message
@@ -173,5 +216,35 @@ touch "${MAIL_DIR}"/.git # change .git's mtime for notmuch new to rescan.
 output=$(NOTMUCH_NEW 2>&1)
 test_expect_equal "$output" "Added 1 new message to the database."
 
+test_begin_subtest "Ignore files and directories specified in new.ignore (multiple occurrences)"
+notmuch config set new.ignore .git ignored_file .ignored_hidden_file
+touch "${MAIL_DIR}"/.git # change .git's mtime for notmuch new to rescan.
+mkdir -p "${MAIL_DIR}"/one/two/three/.git
+notmuch new > /dev/null # ensure that files/folders will be printed in ASCII order.
+touch "${MAIL_DIR}"/{one,one/two,one/two/three}/ignored_file
+output=$(NOTMUCH_NEW --debug 2>&1 | sort)
+test_expect_equal "$output" \
+"(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+No new mail."
+
+
+test_begin_subtest "Don't stop for ignored broken symlinks"
+notmuch config set new.ignore .git ignored_file .ignored_hidden_file broken_link
+ln -s i_do_not_exist "${MAIL_DIR}"/broken_link
+output=$(NOTMUCH_NEW 2>&1)
+test_expect_equal "$output" "No new mail."
 
 test_done
index bfad5d3c79d5b65a8d778190b8721bbf249aada7..ca9c3dcbdcfdb514179c80bf2fca89e469ac4f27 100755 (executable)
@@ -31,6 +31,8 @@ TESTS="
   excludes
   tagging
   json
+  sexp
+  text
   multipart
   thread-naming
   raw
@@ -58,6 +60,10 @@ TESTS="
   emacs-address-cleaning
   emacs-hello
   emacs-show
+  missing-headers
+  hex-escaping
+  parse-time-string
+  search-date
 "
 TESTS=${NOTMUCH_TESTS:=$TESTS}
 
@@ -77,6 +83,12 @@ trap 'e=$?; kill $!; exit $e' HUP INT TERM
 for test in $TESTS; do
     $TEST_TIMEOUT_CMD ./$test "$@" &
     wait $!
+    # If the test failed without producing results, then it aborted,
+    # so we should abort, too.
+    RES=$?
+    if [[ $RES != 0 && ! -e "test-results/${test%.sh}" ]]; then
+        exit $RES
+    fi
 done
 trap - HUP INT TERM
 
diff --git a/test/parse-time-string b/test/parse-time-string
new file mode 100755 (executable)
index 0000000..8ae0b4c
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+test_description="date/time parser module"
+. ./test-lib.sh
+
+# Sanity/smoke tests for the date/time parser independent of notmuch
+
+_date ()
+{
+    date -d "$*" +%s
+}
+
+_parse_time ()
+{
+    ${TEST_DIRECTORY}/parse-time --format=%s "$*"
+}
+
+test_begin_subtest "date(1) default format without TZ code"
+test_expect_equal "$(_parse_time Fri Aug 3 23:06:06 2012)" "$(_date Fri Aug 3 23:06:06 2012)"
+
+test_begin_subtest "date(1) --rfc-2822 format"
+test_expect_equal "$(_parse_time Fri, 03 Aug 2012 23:07:46 +0100)" "$(_date Fri, 03 Aug 2012 23:07:46 +0100)"
+
+test_begin_subtest "date(1) --rfc=3339=seconds format"
+test_expect_equal "$(_parse_time 2012-08-03 23:09:37+03:00)" "$(_date 2012-08-03 23:09:37+03:00)"
+
+test_begin_subtest "Date parser tests"
+REFERENCE=$(_date Tue Jan 11 11:11:00 +0000 2011)
+cat <<EOF > INPUT
+now          ==> Tue Jan 11 11:11:00 +0000 2011
+2010-1-1     ==> ERROR: DATEFORMAT
+Jan 2        ==> Sun Jan 02 11:11:00 +0000 2011
+Mon          ==> Mon Jan 10 11:11:00 +0000 2011
+last Friday  ==> ERROR: FORMAT
+2 hours ago  ==> Tue Jan 11 09:11:00 +0000 2011
+last month   ==> Sat Dec 11 11:11:00 +0000 2010
+month ago    ==> Sat Dec 11 11:11:00 +0000 2010
+two mo       ==> Thu Nov 11 11:11:00 +0000 2010
+3M           ==> Mon Oct 11 11:11:00 +0000 2010
+4-mont       ==> Sat Sep 11 11:11:00 +0000 2010
+5m           ==> Tue Jan 11 11:06:00 +0000 2011
+dozen mi     ==> Tue Jan 11 10:59:00 +0000 2011
+8am          ==> Tue Jan 11 08:00:00 +0000 2011
+9:15         ==> Tue Jan 11 09:15:00 +0000 2011
+12:34        ==> Tue Jan 11 12:34:00 +0000 2011
+monday       ==> Mon Jan 10 11:11:00 +0000 2011
+yesterday    ==> Mon Jan 10 11:11:00 +0000 2011
+tomorrow     ==> ERROR: KEYWORD
+             ==> Tue Jan 11 11:11:00 +0000 2011 # empty string is reference time
+
+Aug 3 23:06:06 2012             ==> Fri Aug 03 23:06:06 +0000 2012 # date(1) default format without TZ code
+Fri, 03 Aug 2012 23:07:46 +0100 ==> Fri Aug 03 22:07:46 +0000 2012 # rfc-2822
+2012-08-03 23:09:37+03:00       ==> Fri Aug 03 20:09:37 +0000 2012 # rfc-3339 seconds
+
+10s           ==> Tue Jan 11 11:10:50 +0000 2011
+19701223s     ==> Fri May 28 10:37:17 +0000 2010
+19701223      ==> Wed Dec 23 11:11:00 +0000 1970
+
+19701223 +0100 ==> Wed Dec 23 11:11:00 +0000 1970 # Timezone is ignored without an error
+
+today ==^^> Wed Jan 12 00:00:00 +0000 2011
+today ==^> Tue Jan 11 23:59:59 +0000 2011
+today ==_> Tue Jan 11 00:00:00 +0000 2011
+
+this week ==^^> Sun Jan 16 00:00:00 +0000 2011
+this week ==^> Sat Jan 15 23:59:59 +0000 2011
+this week ==_> Sun Jan 09 00:00:00 +0000 2011
+
+two months ago ==> Thu Nov 11 11:11:00 +0000 2010
+two months ==> Thu Nov 11 11:11:00 +0000 2010
+
+@1348569850 ==> Tue Sep 25 10:44:10 +0000 2012
+@10 ==> Thu Jan 01 00:00:10 +0000 1970
+EOF
+
+${TEST_DIRECTORY}/parse-time --ref=${REFERENCE} < INPUT > OUTPUT
+test_expect_equal_file INPUT OUTPUT
+
+test_done
diff --git a/test/parse-time.c b/test/parse-time.c
new file mode 100644 (file)
index 0000000..901a4dd
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#include <assert.h>
+#include <ctype.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "parse-time-string.h"
+
+#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))
+
+static const char *parse_time_error_strings[] = {
+    [PARSE_TIME_OK]                    = "OK",
+    [PARSE_TIME_ERR]                   = "ERR",
+    [PARSE_TIME_ERR_LIB]               = "LIB",
+    [PARSE_TIME_ERR_ALREADYSET]                = "ALREADYSET",
+    [PARSE_TIME_ERR_FORMAT]            = "FORMAT",
+    [PARSE_TIME_ERR_DATEFORMAT]                = "DATEFORMAT",
+    [PARSE_TIME_ERR_TIMEFORMAT]                = "TIMEFORMAT",
+    [PARSE_TIME_ERR_INVALIDDATE]       = "INVALIDDATE",
+    [PARSE_TIME_ERR_INVALIDTIME]       = "INVALIDTIME",
+    [PARSE_TIME_ERR_KEYWORD]           = "KEYWORD",
+};
+
+static const char *
+parse_time_strerror (unsigned int errnum)
+{
+    if (errnum < ARRAY_SIZE (parse_time_error_strings))
+       return parse_time_error_strings[errnum];
+    else
+       return NULL;
+}
+
+/*
+ * concat argv[start]...argv[end - 1], separating them by a single
+ * space, to a malloced string
+ */
+static char *
+concat_args (int start, int end, char *argv[])
+{
+    int i;
+    size_t len = 1;
+    char *p;
+
+    for (i = start; i < end; i++)
+       len += strlen (argv[i]) + 1;
+
+    p = malloc (len);
+    if (!p)
+       return NULL;
+
+    *p = 0;
+
+    for (i = start; i < end; i++) {
+       if (i != start)
+           strcat (p, " ");
+       strcat (p, argv[i]);
+    }
+
+    return p;
+}
+
+#define DEFAULT_FORMAT "%a %b %d %T %z %Y"
+
+static void
+usage (const char *name)
+{
+    printf ("Usage: %s [options ...] [<date/time>]\n\n", name);
+    printf (
+       "Parse <date/time> and display it in given format. If <date/time> is\n"
+       "not given, parse each line in stdin according to:\n\n"
+       "  <date/time> [(==>|==_>|==^>|==^^>)<ignored>] [#<comment>]\n\n"
+       "and produce output:\n\n"
+       "  <date/time> (==>|==_>|==^>|==^^>) <time in --format=FMT> [#<comment>]\n\n"
+       "preserving whitespace and comment in input. The operators ==>, ==_>,\n"
+       "==^>, and ==^^> define rounding as no rounding, round down, round up\n"
+       "inclusive, and round up, respectively.\n\n"
+
+       "  -f, --format=FMT output format, FMT according to strftime(3)\n"
+       "                   (default: \"%s\")\n"
+       "  -r, --ref=N      use N seconds since epoch as reference time\n"
+       "                   (default: now)\n"
+       "  -u, --^          round result up inclusive (default: no rounding)\n"
+       "  -U, --^^         round result up (default: no rounding)\n"
+       "  -d, --_          round result down (default: no rounding)\n"
+       "  -h, --help       print this help\n",
+       DEFAULT_FORMAT);
+}
+
+struct {
+    const char *operator;
+    int round;
+} operators[] = {
+    { "==>",   PARSE_TIME_NO_ROUND },
+    { "==_>",  PARSE_TIME_ROUND_DOWN },
+    { "==^>",  PARSE_TIME_ROUND_UP_INCLUSIVE },
+    { "==^^>", PARSE_TIME_ROUND_UP },
+};
+
+static const char *
+find_operator_in_string (char *str, char **ptr, int *round)
+{
+    const char *oper = NULL;
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE (operators); i++) {
+       char *p = strstr (str, operators[i].operator);
+       if (p) {
+           if (round)
+               *round = operators[i].round;
+           if (ptr)
+               *ptr = p;
+
+           oper = operators[i].operator;
+           break;
+       }
+    }
+
+    return oper;
+}
+
+static const char *
+get_operator (int round)
+{
+    const char *oper = NULL;
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE(operators); i++) {
+       if (round == operators[i].round) {
+           oper = operators[i].operator;
+           break;
+       }
+    }
+
+    return oper;
+}
+
+static int
+parse_stdin (FILE *infile, time_t *ref, int round, const char *format)
+{
+    char *input = NULL;
+    char result[1024];
+    size_t inputsize;
+    ssize_t len;
+    struct tm tm;
+    time_t t;
+    int r;
+
+    while ((len = getline (&input, &inputsize, infile)) != -1) {
+       const char *oper;
+       char *trail, *tmp;
+
+       /* trail is trailing whitespace and (optional) comment */
+       trail = strchr (input, '#');
+       if (!trail)
+           trail = input + len;
+
+       while (trail > input && isspace ((unsigned char) *(trail-1)))
+           trail--;
+
+       if (trail == input) {
+           printf ("%s", input);
+           continue;
+       }
+
+       tmp = strdup (trail);
+       if (!tmp) {
+           fprintf (stderr, "strdup() failed\n");
+           continue;
+       }
+       *trail = '\0';
+       trail = tmp;
+
+       /* operator */
+       oper = find_operator_in_string (input, &tmp, &round);
+       if (oper) {
+           *tmp = '\0';
+       } else {
+           oper = get_operator (round);
+           assert (oper);
+       }
+
+       r = parse_time_string (input, &t, ref, round);
+       if (!r) {
+           if (!localtime_r (&t, &tm)) {
+               fprintf (stderr, "localtime_r() failed\n");
+               free (trail);
+               continue;
+           }
+
+           strftime (result, sizeof (result), format, &tm);
+       } else {
+           const char *errstr = parse_time_strerror (r);
+           if (errstr)
+               snprintf (result, sizeof (result), "ERROR: %s", errstr);
+           else
+               snprintf (result, sizeof (result), "ERROR: %d", r);
+       }
+
+       printf ("%s%s %s%s", input, oper, result, trail);
+       free (trail);
+    }
+
+    free (input);
+
+    return 0;
+}
+
+int
+main (int argc, char *argv[])
+{
+    int r;
+    struct tm tm;
+    time_t result;
+    time_t now;
+    time_t *nowp = NULL;
+    char *argstr;
+    int round = PARSE_TIME_NO_ROUND;
+    char buf[1024];
+    const char *format = DEFAULT_FORMAT;
+    struct option options[] = {
+       { "help",       no_argument,            NULL,   'h' },
+       { "^",          no_argument,            NULL,   'u' },
+       { "^^",         no_argument,            NULL,   'U' },
+       { "_",          no_argument,            NULL,   'd' },
+       { "format",     required_argument,      NULL,   'f' },
+       { "ref",        required_argument,      NULL,   'r' },
+       { NULL, 0, NULL, 0 },
+    };
+
+    for (;;) {
+       int c;
+
+       c = getopt_long (argc, argv, "huUdf:r:", options, NULL);
+       if (c == -1)
+           break;
+
+       switch (c) {
+       case 'f':
+           /* output format */
+           format = optarg;
+           break;
+       case 'u':
+           round = PARSE_TIME_ROUND_UP_INCLUSIVE;
+           break;
+       case 'U':
+           round = PARSE_TIME_ROUND_UP;
+           break;
+       case 'd':
+           round = PARSE_TIME_ROUND_DOWN;
+           break;
+       case 'r':
+           /* specify now in seconds since epoch */
+           now = (time_t) strtol (optarg, NULL, 10);
+           if (now >= (time_t) 0)
+               nowp = &now;
+           break;
+       case 'h':
+       case '?':
+       default:
+           usage (argv[0]);
+           return 1;
+       }
+    }
+
+    if (optind == argc)
+       return parse_stdin (stdin, nowp, round, format);
+
+    argstr = concat_args (optind, argc, argv);
+    if (!argstr)
+       return 1;
+
+    r = parse_time_string (argstr, &result, nowp, round);
+
+    free (argstr);
+
+    if (r) {
+       const char *errstr = parse_time_strerror (r);
+       if (errstr)
+           fprintf (stderr, "ERROR: %s\n", errstr);
+       else
+           fprintf (stderr, "ERROR: %d\n", r);
+
+       return r;
+    }
+
+    if (!localtime_r (&result, &tm))
+       return 1;
+
+    strftime (buf, sizeof (buf), format, &tm);
+    printf ("%s\n", buf);
+
+    return 0;
+}
diff --git a/test/random-corpus.c b/test/random-corpus.c
new file mode 100644 (file)
index 0000000..8b7748e
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * Generate a random corpus of stub messages.
+ *
+ * Initial use case is testing dump and restore, so we only have
+ * message-ids and tags.
+ *
+ * Generated message-id's and tags are intentionally nasty.
+ *
+ * Copyright (c) 2012 David Bremner
+ *
+ * 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: David Bremner <david@tethera.net>
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+#include <talloc.h>
+#include <string.h>
+#include <glib.h>
+#include <math.h>
+
+#include "notmuch-client.h"
+#include "command-line-arguments.h"
+#include "database-test.h"
+
+/* Current largest Unicode value defined. Note that most of these will
+ * be printed as boxes in most fonts.
+ */
+
+#define GLYPH_MAX 0x10FFFE
+
+
+typedef struct {
+    int weight;
+    int start;
+    int stop;
+} char_class_t;
+
+/*
+ *  Choose about half ascii as test characters, as ascii
+ *  punctation and whitespace is the main cause of problems for
+ *  the (old) restore parser.
+ *
+ *  We then favour code points with 2 byte encodings. Note that
+ *  code points 0xD800-0xDFFF are forbidden in UTF-8.
+ */
+
+static const
+char_class_t char_class[] = { { 0.50 * GLYPH_MAX, 0x0001, 0x007f },
+                             { 0.75 * GLYPH_MAX, 0x0080, 0x07ff },
+                             { 0.88 * GLYPH_MAX, 0x0800, 0xd7ff },
+                             { 0.90 * GLYPH_MAX, 0xE000, 0xffff },
+                             {        GLYPH_MAX, 0x10000, GLYPH_MAX } };
+
+static gunichar
+random_unichar ()
+{
+    int i;
+    int class = random () % GLYPH_MAX;
+    int size;
+
+    for (i = 0; char_class[i].weight < class; i++) /* nothing */;
+
+    size = char_class[i].stop - char_class[i].start + 1;
+
+    return char_class[i].start + (random () % size);
+}
+
+static char *
+random_utf8_string (void *ctx, size_t char_count)
+{
+    size_t offset = 0;
+    size_t i;
+    gchar *buf = NULL;
+    size_t buf_size = 0;
+
+    for (i = 0; i < char_count; i++) {
+       gunichar randomchar;
+       size_t written;
+
+       /* 6 for one glyph, one for null, one for luck */
+       while (buf_size <= offset + 8) {
+           buf_size = 2 * buf_size + 8;
+           buf = talloc_realloc (ctx, buf, gchar, buf_size);
+       }
+
+       do {
+           randomchar = random_unichar ();
+       } while (randomchar == '\n');
+
+       written = g_unichar_to_utf8 (randomchar, buf + offset);
+
+       if (written <= 0) {
+           fprintf (stderr, "error converting to utf8\n");
+           exit (1);
+       }
+
+       offset += written;
+
+    }
+    buf[offset] = 0;
+    return buf;
+}
+
+
+int
+main (int argc, char **argv)
+{
+
+    void *ctx = talloc_new (NULL);
+
+    char *config_path  = NULL;
+    notmuch_config_t *config;
+    notmuch_database_t *notmuch;
+
+    int num_messages = 500;
+    int max_tags = 10;
+    // leave room for UTF-8 encoding.
+    int tag_len = NOTMUCH_TAG_MAX / 6;
+    // NOTMUCH_MESSAGE_ID_MAX is not exported, so we make a
+    // conservative guess.
+    int message_id_len = (NOTMUCH_TAG_MAX - 20) / 6;
+
+    int seed = 734569;
+
+    notmuch_opt_desc_t options[] = {
+       { NOTMUCH_OPT_STRING, &config_path, "config-path", 'c', 0 },
+       { NOTMUCH_OPT_INT, &num_messages, "num-messages", 'n', 0 },
+       { NOTMUCH_OPT_INT, &max_tags, "max-tags", 'm', 0 },
+       { NOTMUCH_OPT_INT, &message_id_len, "message-id-len", 'M', 0 },
+       { NOTMUCH_OPT_INT, &tag_len, "tag-len", 't', 0 },
+       { NOTMUCH_OPT_INT, &seed, "seed", 's', 0 },
+       { 0, 0, 0, 0, 0 }
+    };
+
+    int opt_index = parse_arguments (argc, argv, options, 1);
+
+    if (opt_index < 0)
+       exit (1);
+
+    if (message_id_len < 1) {
+       fprintf (stderr, "message id's must be least length 1\n");
+       exit (1);
+    }
+
+    if (config_path == NULL) {
+       fprintf (stderr, "configuration path must be specified");
+       exit (1);
+    }
+
+    config = notmuch_config_open (ctx, config_path, NULL);
+    if (config == NULL)
+       return 1;
+
+    if (notmuch_database_open (notmuch_config_get_database_path (config),
+                              NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
+       return 1;
+
+    srandom (seed);
+
+    int count;
+    for (count = 0; count < num_messages; count++) {
+       int j;
+       /* explicitly allow zero tags */
+       int num_tags = random () % (max_tags + 1);
+       /* message ids should be non-empty */
+       int this_mid_len = (random () % message_id_len) + 1;
+       const char **tag_list;
+       char *mid;
+       notmuch_status_t status;
+
+       do {
+           mid = random_utf8_string (ctx, this_mid_len);
+
+           tag_list = talloc_realloc (ctx, NULL, const char *, num_tags + 1);
+
+           for (j = 0; j < num_tags; j++) {
+               int this_tag_len = random () % tag_len + 1;
+
+               tag_list[j] = random_utf8_string (ctx, this_tag_len);
+           }
+
+           tag_list[j] = NULL;
+
+           status = notmuch_database_add_stub_message (notmuch, mid, tag_list);
+       } while (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID);
+
+       if (status != NOTMUCH_STATUS_SUCCESS) {
+           fprintf (stderr, "error %d adding message", status);
+           exit (status);
+       }
+    }
+
+    notmuch_database_destroy (notmuch);
+
+    talloc_free (ctx);
+
+    return 0;
+}
index 00f4beadea8b8947b9f58730611d97991eaa4175..ee5d3618671c5ee33db5843edf1ec631040bf814 100755 (executable)
@@ -138,4 +138,59 @@ References: <${gen_msg_id}>
 
 On Tue, 05 Jan 2010 15:43:56 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
 > 200-byte header"
+
+test_begin_subtest "From guessing: Envelope-To"
+add_message '[from]="Sender <sender@example.com>"' \
+           '[to]="Recipient <recipient@example.com>"' \
+           '[subject]="From guessing"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="From guessing"' \
+           '[header]="Envelope-To: test_suite_other@notmuchmail.org"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: From guessing
+To: Sender <sender@example.com>, Recipient <recipient@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
+> From guessing"
+
+test_begin_subtest "From guessing: X-Original-To"
+add_message '[from]="Sender <sender@example.com>"' \
+           '[to]="Recipient <recipient@example.com>"' \
+           '[subject]="From guessing"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="From guessing"' \
+           '[header]="X-Original-To: test_suite@otherdomain.org"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@otherdomain.org>
+Subject: Re: From guessing
+To: Sender <sender@example.com>, Recipient <recipient@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
+> From guessing"
+
+test_begin_subtest "From guessing: Delivered-To"
+add_message '[from]="Sender <sender@example.com>"' \
+           '[to]="Recipient <recipient@example.com>"' \
+           '[subject]="From guessing"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="From guessing"' \
+           '[header]="Delivered-To: test_suite_other@notmuchmail.org"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: From guessing
+To: Sender <sender@example.com>, Recipient <recipient@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
+> From guessing"
+
 test_done
diff --git a/test/search-date b/test/search-date
new file mode 100755 (executable)
index 0000000..70bcf34
--- /dev/null
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+test_description="date:since..until queries"
+. ./test-lib.sh
+
+add_email_corpus
+
+test_begin_subtest "Absolute date range"
+output=$(notmuch search date:2010-12-16..12/16/2010 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)"
+
+test_begin_subtest "Absolute time range with TZ"
+notmuch search date:18-Nov-2009_02:19:26-0800..2009-11-18_04:49:52-06:00 | notmuch_search_sanitize > OUTPUT
+cat <<EOF >EXPECTED
+thread:XXX   2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+thread:XXX   2009-11-18 [1/3] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_done
index 8b57a432438e06ea6de9e8aebb8e29b2d01e29a7..c2a87eb1ca27ceed2d9b290e26bc1efef65bf89c 100755 (executable)
@@ -62,7 +62,7 @@ cat <<EOF >EXPECTED
 "THREADID",
 "THREADID"]
 EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
 test_begin_subtest "--output=messages"
 notmuch search --output=messages '*' >OUTPUT
diff --git a/test/sexp b/test/sexp
new file mode 100755 (executable)
index 0000000..492a82f
--- /dev/null
+++ b/test/sexp
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+test_description="--format=sexp output"
+. ./test-lib.sh
+
+test_begin_subtest "Show message: sexp"
+add_message "[subject]=\"sexp-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"sexp-show-message\""
+output=$(notmuch show --format=sexp "sexp-show-message")
+test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename \"${gen_msg_filename}\" :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\"))) ())))"
+
+# This should be the same output as above.
+test_begin_subtest "Show message: sexp --body=true"
+output=$(notmuch show --format=sexp --body=true "sexp-show-message")
+test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename \"${gen_msg_filename}\" :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\"))) ())))"
+
+test_begin_subtest "Show message: sexp --body=false"
+output=$(notmuch show --format=sexp --body=false "sexp-show-message")
+test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename \"${gen_msg_filename}\" :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))"
+
+test_begin_subtest "Search message: sexp"
+add_message "[subject]=\"sexp-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"sexp-search-message\""
+output=$(notmuch search --format=sexp "sexp-search-message" | notmuch_search_sanitize)
+test_expect_equal "$output" "((:thread \"0000000000000002\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-subject\" :tags (\"inbox\" \"unread\")))"
+
+test_begin_subtest "Show message: sexp, utf-8"
+add_message "[subject]=\"sexp-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
+output=$(notmuch show --format=sexp "jsön-show-méssage")
+test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename \"${gen_msg_filename}\" :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :headers (:Subject \"sexp-show-utf8-body-sübjéct\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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: sexp, inline attachment filename"
+subject='sexp-show-inline-attachment-filename'
+id="sexp-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=sexp "id:$id")
+filename=$(notmuch search --output=files "id:$id")
+# Get length of README after base64-encoding, minus additional newline.
+attachment_length=$(( $(base64 $TEST_DIRECTORY/README | wc -c) - 1 ))
+test_expect_equal "$output" "((((:id \"$id\" :match t :excluded nil :filename \"$filename\" :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\") :headers (:Subject \"sexp-show-inline-attachment-filename\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"test_suite@notmuchmail.org\" :Date \"Sat, 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\" :content-transfer-encoding \"base64\" :content-length $attachment_length))))) ())))"
+
+test_begin_subtest "Search message: sexp, utf-8"
+add_message "[subject]=\"sexp-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=sexp "jsön-search-méssage" | notmuch_search_sanitize)
+test_expect_equal "$output" "((:thread \"0000000000000005\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :tags (\"inbox\" \"unread\")))"
+
+
+test_done
index 3801a5e066c7c8fe1804ca3306e250fb0eb962f7..bb136687b2fd31a29775d3a25550c1f795ec034a 100644 (file)
@@ -37,7 +37,9 @@
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
-#include <netinet/ip.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
 #include <netdb.h>
 #include <unistd.h>
 
@@ -117,6 +119,7 @@ do_smtp_to_file (FILE *peer, FILE *output)
 int
 main (int argc, char *argv[])
 {
+       const char * progname;
        char *output_filename;
        FILE *peer_file, *output;
        int sock, peer, err;
@@ -124,9 +127,31 @@ main (int argc, char *argv[])
        struct hostent *hostinfo;
        socklen_t peer_addr_len;
        int reuse;
+       int background;
+
+       progname = argv[0];
+
+       background = 0;
+       for (; argc >= 2; argc--, argv++) {
+               if (argv[1][0] != '-')
+                       break;
+               if (strcmp (argv[1], "--") == 0) {
+                       argc--;
+                       argv++;
+                       break;
+               }
+               if (strcmp (argv[1], "--background") == 0) {
+                       background = 1;
+                       continue;
+               }
+               fprintf(stderr, "%s: unregognized option '%s'\n",
+                       progname, argv[1]);
+               return 1;
+       }
 
        if (argc != 2) {
-               fprintf (stderr, "Usage: %s <output-file>\n", argv[0]);
+               fprintf (stderr,
+                        "Usage: %s [--background] <output-file>\n", progname);
                return 1;
        }
 
@@ -179,6 +204,36 @@ main (int argc, char *argv[])
                return 1;
        }
 
+       if (background) {
+               int pid = fork ();
+               if (pid > 0) {
+                       printf ("smtp_dummy_pid='%d'\n", pid);
+                       fflush (stdout);
+                       close (sock);
+                       return 0;
+               }
+               if (pid < 0) {
+                       fprintf (stderr, "Error: fork() failed: %s\n",
+                                strerror (errno));
+                       close (sock);
+                       return 1;
+               }
+               /* Reached if pid == 0 (the child process). */
+               /* Close stdout so that the one interested in pid value will
+                  also get EOF. */
+               close (STDOUT_FILENO);
+               /* dup2() will re-reserve fd of stdout (1) (opportunistically),
+                  in case fd of stderr (2) is open. If that was not open we
+                  don't care fd of stdout (1) either. */
+               dup2 (STDERR_FILENO, STDOUT_FILENO);
+
+               /* This process is now out of reach of shell's job control.
+                  To resolve the rare but possible condition where this
+                  "daemon" is started but never connected this process will
+                  (only) have 30 seconds to exist. */
+               alarm (30);
+       }
+
        peer_addr_len = sizeof (peer_addr);
        peer = accept (sock, (struct sockaddr *) &peer_addr, &peer_addr_len);
        if (peer == -1) {
index e4782ed490d70c536a6c4bfdf94397416fffaf77..1f5632cb3da838dbcae52658301c53efc097bd2f 100755 (executable)
@@ -46,4 +46,203 @@ 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 tag4 unread)"
 
+test_begin_subtest "--batch"
+notmuch tag --batch <<EOF
+# %20 is a space in tag
+-:"%20 -tag1 +tag5 +tag6 -- One
++tag1 -tag1 -tag4 +tag4 -- Two
+-tag6 One
++tag5 Two
+EOF
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag5 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag4 tag5 unread)"
+
+# generate a common input file for the next several tests.
+cat > batch.in  <<EOF
+# %40 is an @ in tag
++%40 -tag5 +tag6 -- One
++tag1 -tag1 -tag4 +tag4 -- Two
+-tag5 +tag6 Two
+EOF
+
+cat > batch.expected <<EOF
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (@ inbox tag6 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag4 tag6 unread)
+EOF
+
+test_begin_subtest "--input"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag --input=batch.in
+notmuch search \* | notmuch_search_sanitize > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+test_expect_equal_file batch.expected OUTPUT
+
+test_begin_subtest "--batch --input"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag --batch --input=batch.in
+notmuch search \* | notmuch_search_sanitize > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+test_expect_equal_file batch.expected OUTPUT
+
+test_begin_subtest "--batch, blank lines and comments"
+notmuch dump | sort > EXPECTED
+notmuch tag --batch <<EOF
+# this line is a comment; the next has only white space
+        
+
+# the previous line is empty
+EOF
+notmuch dump | sort > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: checking error messages'
+notmuch dump --format=batch-tag > BACKUP
+notmuch tag --batch <<EOF 2>OUTPUT
+# the next line has a space
+# this line has no tag operations, but this is permitted in batch format.
+a
++0
++a +b
+# trailing whitespace
++a +b 
++c +d --
+# this is a harmless comment, do not yell about it.
+
+# the previous line was blank; also no yelling please
++%zz -- id:whatever
+# the next non-comment line should report an an empty tag error for
+# batch tagging, but not for restore
++ +e -- id:foo
++- -- id:foo
+EOF
+
+cat <<EOF > EXPECTED
+Warning: no query string [+0]
+Warning: no query string [+a +b]
+Warning: missing query string [+a +b ]
+Warning: no query string after -- [+c +d --]
+Warning: hex decoding of tag %zz failed [+%zz -- id:whatever]
+Warning: empty tag forbidden [+ +e -- id:foo]
+Warning: tag starting with '-' forbidden [+- -- id:foo]
+EOF
+
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: tags with quotes'
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++%22%27%22%27%22%22%27%27 -- One
+-%22%27%22%27%22%22%27%27 -- One
++%22%27%22%22%22%27 -- One
++%22%27%22%27%22%22%27%27 -- Two
+EOF
+
+cat <<EOF > EXPECTED
++%22%27%22%22%22%27 +inbox +tag5 +unread -- id:msg-001@notmuch-test-suite
++%22%27%22%27%22%22%27%27 +inbox +tag4 +tag5 +unread -- id:msg-002@notmuch-test-suite
+EOF
+
+notmuch dump --format=batch-tag | sort > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: tags with punctuation and space'
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- One
+-%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- One
++%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%20%60%7e -- Two
+-%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%20%60%7e -- Two
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- One
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- Two
+EOF
+
+cat <<EOF > EXPECTED
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e +inbox +tag4 +tag5 +unread -- id:msg-002@notmuch-test-suite
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e +inbox +tag5 +unread -- id:msg-001@notmuch-test-suite
+EOF
+
+notmuch dump --format=batch-tag | sort > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: unicode tags'
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7 -- One
++=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d -- One
++A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27 -- One
++R -- One
++%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6 -- One
++%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d -- One
++L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1 -- One
++P%c4%98%2f -- One
++%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d -- One
++%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b -- One
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7  +=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d  +A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27  +R  +%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6  +%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d  +L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1  +P%c4%98%2f  +%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d  +%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b -- Two
+EOF
+
+cat <<EOF > EXPECTED
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7 +=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d +A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27 +L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1 +P%c4%98%2f +R +inbox +tag4 +tag5 +unread +%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d +%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b +%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6 +%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d -- id:msg-002@notmuch-test-suite
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7 +=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d +A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27 +L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1 +P%c4%98%2f +R +inbox +tag5 +unread +%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d +%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b +%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6 +%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d -- id:msg-001@notmuch-test-suite
+EOF
+
+notmuch dump --format=batch-tag | sort > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--batch: only space and % needs to be encoded."
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++winner *
++foo::bar%25 -- (One and Two) or (One and tag:winner)
++found::it -- tag:foo::bar%
+# ignore this line and the next
+
++space%20in%20tags -- Two
+# add tag '(tags)', among other stunts.
++crazy{ +(tags) +&are +#possible\ -- tag:"space in tags"
++match*crazy -- tag:crazy{
++some_tag -- id:"this is ""nauty)"""
+EOF
+
+cat <<EOF > EXPECTED
++%23possible%5c +%26are +%28tags%29 +crazy%7b +inbox +match%2acrazy +space%20in%20tags +tag4 +tag5 +unread +winner -- id:msg-002@notmuch-test-suite
++foo%3a%3abar%25 +found%3a%3ait +inbox +tag5 +unread +winner -- id:msg-001@notmuch-test-suite
+EOF
+
+notmuch dump --format=batch-tag | sort > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: unicode message-ids'
+
+${TEST_DIRECTORY}/random-corpus --config-path=${NOTMUCH_CONFIG} \
+     --num-messages=100
+
+notmuch dump --format=batch-tag | sed 's/^.* -- /+common_tag -- /' | \
+    sort > EXPECTED
+
+notmuch dump --format=batch-tag | sed 's/^.* -- /  -- /' | \
+    notmuch restore --format=batch-tag
+
+notmuch tag --batch < EXPECTED
+
+notmuch dump --format=batch-tag| \
+    sort > OUTPUT
+
+test_expect_equal_file EXPECTED OUTPUT
+
+test_expect_code 1 "Empty tag names" 'notmuch tag + One'
+
+test_expect_code 1 "Tag name beginning with -" 'notmuch tag +- One'
+
 test_done
diff --git a/test/test-lib-common.sh b/test/test-lib-common.sh
new file mode 100644 (file)
index 0000000..e1eaa5a
--- /dev/null
@@ -0,0 +1,147 @@
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+# 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, see http://www.gnu.org/licenses/ .
+
+# This file contains common code to be used by both the regular
+# (correctness) tests and the performance tests.
+
+find_notmuch_path ()
+{
+    dir="$1"
+
+    while [ -n "$dir" ]; do
+       bin="$dir/notmuch"
+       if [ -x "$bin" ]; then
+           echo "$dir"
+           return
+       fi
+       dir="$(dirname "$dir")"
+       if [ "$dir" = "/" ]; then
+           break
+       fi
+    done
+}
+
+# Test the binaries we have just built.  The tests are kept in
+# test/ subdirectory and are run in 'trash directory' subdirectory.
+TEST_DIRECTORY=$(pwd)
+notmuch_path=`find_notmuch_path "$TEST_DIRECTORY"`
+if test -n "$valgrind"
+then
+       make_symlink () {
+               test -h "$2" &&
+               test "$1" = "$(readlink "$2")" || {
+                       # be super paranoid
+                       if mkdir "$2".lock
+                       then
+                               rm -f "$2" &&
+                               ln -s "$1" "$2" &&
+                               rm -r "$2".lock
+                       else
+                               while test -d "$2".lock
+                               do
+                                       say "Waiting for lock on $2."
+                                       sleep 1
+                               done
+                       fi
+               }
+       }
+
+       make_valgrind_symlink () {
+               # handle only executables
+               test -x "$1" || return
+
+               base=$(basename "$1")
+               symlink_target=$TEST_DIRECTORY/../$base
+               # do not override scripts
+               if test -x "$symlink_target" &&
+                   test ! -d "$symlink_target" &&
+                   test "#!" != "$(head -c 2 < "$symlink_target")"
+               then
+                       symlink_target=$TEST_DIRECTORY/valgrind.sh
+               fi
+               case "$base" in
+               *.sh|*.perl)
+                       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
+       }
+
+       # override notmuch executable in TEST_DIRECTORY/..
+       GIT_VALGRIND=$TEST_DIRECTORY/valgrind
+       mkdir -p "$GIT_VALGRIND"/bin
+       make_valgrind_symlink $TEST_DIRECTORY/../notmuch
+       OLDIFS=$IFS
+       IFS=:
+       for path in $PATH
+       do
+               ls "$path"/notmuch 2> /dev/null |
+               while read file
+               do
+                       make_valgrind_symlink "$file"
+               done
+       done
+       IFS=$OLDIFS
+       PATH=$GIT_VALGRIND/bin:$PATH
+       GIT_EXEC_PATH=$GIT_VALGRIND/bin
+       export GIT_VALGRIND
+       test -n "$notmuch_path" && MANPATH="$notmuch_path/man:$MANPATH"
+else # normal case
+       if test -n "$notmuch_path"
+               then
+                       PATH="$notmuch_path:$PATH"
+                       MANPATH="$notmuch_path/man:$MANPATH"
+               fi
+fi
+export PATH MANPATH
+
+# Test repository
+test="tmp.$(basename "$0" .sh)"
+test -n "$root" && test="$root/$test"
+case "$test" in
+/*) TMP_DIRECTORY="$test" ;;
+ *) TMP_DIRECTORY="$TEST_DIRECTORY/$test" ;;
+esac
+test ! -z "$debug" || remove_tmp=$TMP_DIRECTORY
+rm -fr "$test" || {
+       GIT_EXIT_OK=t
+       echo >&5 "FATAL: Cannot prepare test area"
+       exit 1
+}
+
+# A temporary home directory is needed by at least:
+# - emacs/"Sending a message via (fake) SMTP"
+# - emacs/"Reply within emacs"
+# - crypto/emacs_deliver_message
+export HOME="${TMP_DIRECTORY}/home"
+mkdir -p "${HOME}"
+
+MAIL_DIR="${TMP_DIRECTORY}/mail"
+export GNUPGHOME="${TMP_DIRECTORY}/gnupg"
+export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config"
+
+mkdir -p "${test}"
+mkdir -p "${MAIL_DIR}"
+
+cat <<EOF >"${NOTMUCH_CONFIG}"
+[database]
+path=${MAIL_DIR}
+
+[user]
+name=Notmuch Test Suite
+primary_email=test_suite@notmuchmail.org
+other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org
+EOF
index 6271da22de4b97c2dbca1d37957a3b437af4833f..dece811e6136064b84afec343161c1e25f5c9dec 100644 (file)
     "Disable yes-or-no-p before executing kill-emacs"
     (defun yes-or-no-p (prompt) t)))
 
+;; Emacs bug #2930:
+;;     23.0.92; `accept-process-output' and `sleep-for' do not run sentinels
+;; seems to be present in Emacs 23.1.
+;; Running `list-processes' after `accept-process-output' seems to work
+;; around this problem.
+(if (and (= emacs-major-version 23) (= emacs-minor-version 1))
+  (defadvice accept-process-output (after run-list-processes activate)
+    "run list-processes after executing accept-process-output"
+    (list-processes)))
+
 (defun notmuch-test-wait ()
   "Wait for process completion."
   (while (get-buffer-process (current-buffer))
-    (sleep-for 0.1)))
+    (accept-process-output nil 0.1)))
 
 (defun test-output (&optional filename)
   "Save current buffer to file FILENAME.  Default FILENAME is OUTPUT."
@@ -89,6 +99,28 @@ nothing."
 (add-hook-counter 'notmuch-hello-mode-hook)
 (add-hook-counter 'notmuch-hello-refresh-hook)
 
+(defadvice notmuch-search-process-filter (around pessimal activate disable)
+  "Feed notmuch-search-process-filter one character at a time."
+  (let ((string (ad-get-arg 1)))
+    (loop for char across string
+         do (progn
+              (ad-set-arg 1 (char-to-string char))
+              ad-do-it))))
+
+(defun notmuch-test-mark-links ()
+  "Enclose links in the current buffer with << and >>."
+  ;; Links are often created by jit-lock functions
+  (jit-lock-fontify-now)
+  (save-excursion
+    (let ((inhibit-read-only t))
+      (goto-char (point-min))
+      (let ((button))
+       (while (setq button (next-button (point)))
+         (goto-char (button-start button))
+         (insert "<<")
+         (goto-char (button-end button))
+         (insert ">>"))))))
+
 (defmacro notmuch-test-run (&rest body)
   "Evaluate a BODY of test expressions and output the result."
   `(with-temp-buffer
index 06aaea270e946cb6811ed6e18258781b5d267b8d..84db79265418535bdc6a9b4a364a65cd5a710b67 100644 (file)
@@ -41,6 +41,10 @@ esac
 # Keep the original TERM for say_color and test_emacs
 ORIGINAL_TERM=$TERM
 
+# dtach(1) provides more capable terminal environment to anything
+# that requires more than dumb terminal...
+[ x"${TERM:-dumb}" = xdumb ] && DTACH_TERM=vt100 || DTACH_TERM=$TERM
+
 # For repeatability, reset the environment to known value.
 LANG=C
 LC_ALL=C
@@ -49,7 +53,13 @@ TZ=UTC
 TERM=dumb
 export LANG LC_ALL PAGER TERM TZ
 GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u}
+if [[ ( -n "$TEST_EMACS" && -z "$TEST_EMACSCLIENT" ) || \
+      ( -z "$TEST_EMACS" && -n "$TEST_EMACSCLIENT" ) ]]; then
+    echo "error: must specify both or neither of TEST_EMACS and TEST_EMACSCLIENT" >&2
+    exit 1
+fi
 TEST_EMACS=${TEST_EMACS:-${EMACS:-emacs}}
+TEST_EMACSCLIENT=${TEST_EMACSCLIENT:-emacsclient}
 
 # Protect ourselves from common misconfiguration to export
 # CDPATH into the environment
@@ -350,6 +360,11 @@ ${additional_headers}"
 ${additional_headers}"
     fi
 
+    if [ ! -z "${template[bcc]}" ]; then
+       additional_headers="Bcc: ${template[bcc]}
+${additional_headers}"
+    fi
+
     if [ ! -z "${template[references]}" ]; then
        additional_headers="References: ${template[references]}
 ${additional_headers}"
@@ -403,8 +418,11 @@ 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}
-    $TEST_DIRECTORY/smtp-dummy sent_message &
-    smtp_dummy_pid=$!
+    # eval'ing smtp-dummy --background will set smtp_dummy_pid
+    smtp_dummy_pid=
+    eval `$TEST_DIRECTORY/smtp-dummy --background sent_message`
+    test -n "$smtp_dummy_pid" || return 1
+
     test_emacs \
        "(let ((message-send-mail-function 'message-smtpmail-send-it)
               (smtpmail-smtp-server \"localhost\")
@@ -419,9 +437,11 @@ emacs_deliver_message ()
           (insert \"${body}\")
           $@
           (message-send-and-exit))"
-    # opportunistically quit smtp-dummy in case above fails.
-    { echo QUIT > /dev/tcp/localhost/25025; } 2>/dev/null
-    wait ${smtp_dummy_pid}
+
+    # In case message was sent properly, client waits for confirmation
+    # before exiting and resuming control here; therefore making sure
+    # that server exits by sending (KILL) signal to it is safe.
+    kill -9 $smtp_dummy_pid
     notmuch new >/dev/null
 }
 
@@ -497,21 +517,38 @@ test_expect_equal_file ()
        test "$#" = 2 ||
        error "bug in the test script: not 2 or 3 parameters to test_expect_equal"
 
-       output="$1"
-       expected="$2"
+       file1="$1"
+       basename1=`basename "$file1"`
+       file2="$2"
+       basename2=`basename "$file2"`
        if ! test_skip "$test_subtest_name"
        then
-               if diff -q "$expected" "$output" >/dev/null ; then
+               if diff -q "$file1" "$file2" >/dev/null ; then
                        test_ok_ "$test_subtest_name"
                else
                        testname=$this_test.$test_count
-                       cp "$output" $testname.output
-                       cp "$expected" $testname.expected
-                       test_failure_ "$test_subtest_name" "$(diff -u $testname.expected $testname.output)"
+                       cp "$file1" "$testname.$basename1"
+                       cp "$file2" "$testname.$basename2"
+                       test_failure_ "$test_subtest_name" "$(diff -u "$testname.$basename1" "$testname.$basename2")"
                fi
     fi
 }
 
+# Like test_expect_equal, but arguments are JSON expressions to be
+# canonicalized before diff'ing.  If an argument cannot be parsed, it
+# is used unchanged so that there's something to diff against.
+test_expect_equal_json () {
+    # The test suite forces LC_ALL=C, but this causes Python 3 to
+    # decode stdin as ASCII.  We need to read JSON in UTF-8, so
+    # override Python's stdio encoding defaults.
+    output=$(echo "$1" | PYTHONIOENCODING=utf-8 python -mjson.tool \
+        || echo "$1")
+    expected=$(echo "$2" | PYTHONIOENCODING=utf-8 python -mjson.tool \
+        || echo "$2")
+    shift 2
+    test_expect_equal "$output" "$expected" "$@"
+}
+
 test_emacs_expect_t () {
        test "$#" = 2 && { prereq=$1; shift; } || prereq=
        test "$#" = 1 ||
@@ -543,12 +580,12 @@ test_emacs_expect_t () {
 
 NOTMUCH_NEW ()
 {
-    notmuch new | grep -v -E -e '^Processed [0-9]*( total)? file|Found [0-9]* total file'
+    notmuch new "${@}" | grep -v -E -e '^Processed [0-9]*( total)? file|Found [0-9]* total file'
 }
 
 notmuch_search_sanitize ()
 {
-    sed -r -e 's/("?thread"?: ?)("?)................("?)/\1\2XXX\3/'
+    perl -pe 's/("?thread"?: ?)("?)................("?)/\1\2XXX\3/'
 }
 
 NOTMUCH_SHOW_FILENAME_SQUELCH='s,filename:.*/mail,filename:/XXX/mail,'
@@ -565,10 +602,9 @@ notmuch_show_sanitize_all ()
 
 notmuch_json_show_sanitize ()
 {
-    sed -e 's|, |,\n |g' | \
-       sed \
-       -e 's|"id": "[^"]*",|"id": "XXXXX",|' \
-       -e 's|"filename": "[^"]*",|"filename": "YYYYY",|'
+    sed \
+       -e 's|"id": "[^"]*",|"id": "XXXXX",|g' \
+       -e 's|"filename": "/[^"]*",|"filename": "YYYYY",|g'
 }
 
 # End of notmuch helper functions
@@ -598,18 +634,22 @@ test_have_prereq () {
        esac
 }
 
+declare -A test_missing_external_prereq_
+declare -A test_subtest_missing_external_prereq_
+
 # declare prerequisite for the given external binary
 test_declare_external_prereq () {
        binary="$1"
        test "$#" = 2 && name=$2 || name="$binary(1)"
 
-       hash $binary 2>/dev/null || eval "
-       test_missing_external_prereq_${binary}_=t
+       if ! hash $binary 2>/dev/null; then
+               test_missing_external_prereq_["${binary}"]=t
+               eval "
 $binary () {
-       echo -n \"\$test_subtest_missing_external_prereqs_ \" | grep -qe \" $name \" ||
-       test_subtest_missing_external_prereqs_=\"\$test_subtest_missing_external_prereqs_ $name\"
+       test_subtest_missing_external_prereq_[\"${name}\"]=t
        false
 }"
+       fi
 }
 
 # Explicitly require external prerequisite.  Useful when binary is
@@ -617,7 +657,7 @@ $binary () {
 # Returns success if dependency is available, failure otherwise.
 test_require_external_prereq () {
        binary="$1"
-       if [ "$(eval echo -n \$test_missing_external_prereq_${binary}_)" = t ]; then
+       if [[ ${test_missing_external_prereq_["${binary}"]} == t ]]; then
                # dependency is missing, call the replacement function to note it
                eval "$binary"
        else
@@ -710,9 +750,9 @@ test_skip () {
 }
 
 test_check_missing_external_prereqs_ () {
-       if test -n "$test_subtest_missing_external_prereqs_"; then
-               say_color skip >&1 "missing prerequisites:"
-               echo "$test_subtest_missing_external_prereqs_" >&1
+       if [[ ${#test_subtest_missing_external_prereq_[@]} != 0 ]]; then
+               say_color skip >&1 "missing prerequisites: "
+               echo ${!test_subtest_missing_external_prereq_[@]} >&1
                test_report_skip_ "$@"
        else
                false
@@ -893,7 +933,7 @@ test_done () {
        GIT_EXIT_OK=t
        test_results_dir="$TEST_DIRECTORY/test-results"
        mkdir -p "$test_results_dir"
-       test_results_path="$test_results_dir/${0%.sh}-$$"
+       test_results_path="$test_results_dir/${0%.sh}"
 
        echo "total $test_count" >> $test_results_path
        echo "success $test_success" >> $test_results_path
@@ -948,7 +988,7 @@ test_emacs () {
        missing_dependencies=
        test_require_external_prereq dtach || missing_dependencies=1
        test_require_external_prereq emacs || missing_dependencies=1
-       test_require_external_prereq emacsclient || missing_dependencies=1
+       test_require_external_prereq ${TEST_EMACSCLIENT} || missing_dependencies=1
        test -z "$missing_dependencies" || return
 
        if [ -z "$EMACS_SERVER" ]; then
@@ -960,9 +1000,10 @@ test_emacs () {
                fi
                server_name="notmuch-test-suite-$$"
                # start a detached session with an emacs server
-               # user's TERM is given to dtach which assumes a minimally
+               # user's TERM (or 'vt100' in case user's TERM is unset, empty
+               # or 'dumb') 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.$$" \
+               TERM=$DTACH_TERM dtach -n "$TEST_TMPDIR/emacs-dtach-socket.$$" \
                        sh -c "stty rows 24 cols 80; exec '$TMP_DIRECTORY/run_emacs' \
                                --no-window-system \
                                $load_emacs_tests \
@@ -976,7 +1017,15 @@ test_emacs () {
                done
        fi
 
-       emacsclient --socket-name="$EMACS_SERVER" --eval "(progn $@)"
+       # Clear test-output output file.  Most Emacs tests end with a
+       # call to (test-output).  If the test code fails with an
+       # exception before this call, the output file won't get
+       # updated.  Since we don't want to compare against an output
+       # file from another test, so start out with an empty file.
+       rm -f OUTPUT
+       touch OUTPUT
+
+       ${TEST_EMACSCLIENT} --socket-name="$EMACS_SERVER" --eval "(progn $@)"
 }
 
 test_python() {
@@ -987,7 +1036,7 @@ test_python() {
        # most others as /usr/bin/python. So first try python2, and fallback to
        # python if python2 doesn't exist.
        cmd=python2
-       [[ "$test_missing_external_prereq_python2_" = t ]] && cmd=python
+       [[ ${test_missing_external_prereq_[python2]} == t ]] && cmd=python
 
        (echo "import sys; _orig_stdout=sys.stdout; sys.stdout=open('OUTPUT', 'w')"; cat) \
                | $cmd -
@@ -1029,7 +1078,7 @@ test_reset_state_ () {
        test -z "$test_init_done_" && test_init_
 
        test_subtest_known_broken_=
-       test_subtest_missing_external_prereqs_=
+       test_subtest_missing_external_prereq_=()
 }
 
 # called once before the first subtest
@@ -1041,129 +1090,7 @@ test_init_ () {
 }
 
 
-find_notmuch_path ()
-{
-    dir="$1"
-
-    while [ -n "$dir" ]; do
-       bin="$dir/notmuch"
-       if [ -x "$bin" ]; then
-           echo "$dir"
-           return
-       fi
-       dir="$(dirname "$dir")"
-       if [ "$dir" = "/" ]; then
-           break
-       fi
-    done
-}
-
-# Test the binaries we have just built.  The tests are kept in
-# test/ subdirectory and are run in 'trash directory' subdirectory.
-TEST_DIRECTORY=$(pwd)
-if test -n "$valgrind"
-then
-       make_symlink () {
-               test -h "$2" &&
-               test "$1" = "$(readlink "$2")" || {
-                       # be super paranoid
-                       if mkdir "$2".lock
-                       then
-                               rm -f "$2" &&
-                               ln -s "$1" "$2" &&
-                               rm -r "$2".lock
-                       else
-                               while test -d "$2".lock
-                               do
-                                       say "Waiting for lock on $2."
-                                       sleep 1
-                               done
-                       fi
-               }
-       }
-
-       make_valgrind_symlink () {
-               # handle only executables
-               test -x "$1" || return
-
-               base=$(basename "$1")
-               symlink_target=$TEST_DIRECTORY/../$base
-               # do not override scripts
-               if test -x "$symlink_target" &&
-                   test ! -d "$symlink_target" &&
-                   test "#!" != "$(head -c 2 < "$symlink_target")"
-               then
-                       symlink_target=$TEST_DIRECTORY/valgrind.sh
-               fi
-               case "$base" in
-               *.sh|*.perl)
-                       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
-       }
-
-       # override notmuch executable in TEST_DIRECTORY/..
-       GIT_VALGRIND=$TEST_DIRECTORY/valgrind
-       mkdir -p "$GIT_VALGRIND"/bin
-       make_valgrind_symlink $TEST_DIRECTORY/../notmuch
-       OLDIFS=$IFS
-       IFS=:
-       for path in $PATH
-       do
-               ls "$path"/notmuch 2> /dev/null |
-               while read file
-               do
-                       make_valgrind_symlink "$file"
-               done
-       done
-       IFS=$OLDIFS
-       PATH=$GIT_VALGRIND/bin:$PATH
-       GIT_EXEC_PATH=$GIT_VALGRIND/bin
-       export GIT_VALGRIND
-else # normal case
-       notmuch_path=`find_notmuch_path "$TEST_DIRECTORY"`
-       test -n "$notmuch_path" && PATH="$notmuch_path:$PATH"
-fi
-export PATH
-
-# Test repository
-test="tmp.$(basename "$0" .sh)"
-test -n "$root" && test="$root/$test"
-case "$test" in
-/*) TMP_DIRECTORY="$test" ;;
- *) TMP_DIRECTORY="$TEST_DIRECTORY/$test" ;;
-esac
-test ! -z "$debug" || remove_tmp=$TMP_DIRECTORY
-rm -fr "$test" || {
-       GIT_EXIT_OK=t
-       echo >&5 "FATAL: Cannot prepare test area"
-       exit 1
-}
-
-# A temporary home directory is needed by at least:
-# - emacs/"Sending a message via (fake) SMTP"
-# - emacs/"Reply within emacs"
-# - crypto/emacs_deliver_message
-export HOME="${TMP_DIRECTORY}/home"
-mkdir -p "${HOME}"
-
-MAIL_DIR="${TMP_DIRECTORY}/mail"
-export GNUPGHOME="${TMP_DIRECTORY}/gnupg"
-export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config"
-
-mkdir -p "${test}"
-mkdir -p "${MAIL_DIR}"
-
-cat <<EOF >"${NOTMUCH_CONFIG}"
-[database]
-path=${MAIL_DIR}
-
-[user]
-name=Notmuch Test Suite
-primary_email=test_suite@notmuchmail.org
-other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org
-EOF
+. ./test-lib-common.sh
 
 emacs_generate_script
 
@@ -1251,7 +1178,7 @@ rm -f y
 # declare prerequisites for external binaries used in tests
 test_declare_external_prereq dtach
 test_declare_external_prereq emacs
-test_declare_external_prereq emacsclient
+test_declare_external_prereq ${TEST_EMACSCLIENT}
 test_declare_external_prereq gdb
 test_declare_external_prereq gpg
 test_declare_external_prereq python
diff --git a/test/text b/test/text
new file mode 100755 (executable)
index 0000000..b5ccefc
--- /dev/null
+++ b/test/text
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+test_description="--format=text output"
+. ./test-lib.sh
+
+test_begin_subtest "Show message: text"
+add_message "[subject]=\"text-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"text-show-message\""
+output=$(notmuch show --format=text "text-show-message" | notmuch_show_sanitize_all)
+test_expect_equal "$output" "\
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2000-01-01) (inbox unread)
+Subject: text-show-subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+text-show-message
+\fpart}
+\fbody}
+\fmessage}"
+
+test_begin_subtest "Search message: text"
+add_message "[subject]=\"text-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"text-search-message\""
+output=$(notmuch search --format=text "text-search-message" | notmuch_search_sanitize)
+test_expect_equal "$output" \
+"thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; text-search-subject (inbox unread)"
+
+test_begin_subtest "Show message: text, utf-8"
+add_message "[subject]=\"text-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"tëxt-show-méssage\""
+output=$(notmuch show --format=text "tëxt-show-méssage" | notmuch_show_sanitize_all)
+test_expect_equal "$output" "\
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2000-01-01) (inbox unread)
+Subject: text-show-utf8-body-sübjéct
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+tëxt-show-méssage
+\fpart}
+\fbody}
+\fmessage}"
+
+test_begin_subtest "Search message: text, utf-8"
+add_message "[subject]=\"text-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"tëxt-search-méssage\""
+output=$(notmuch search --format=text "tëxt-search-méssage" | notmuch_search_sanitize)
+test_expect_equal "$output" \
+"thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; text-search-utf8-body-sübjéct (inbox unread)"
+
+add_email_corpus
+
+test_begin_subtest "Search message tags: text0"
+cat <<EOF > EXPECTED
+attachment inbox signed unread
+EOF
+notmuch search --format=text0 --output=tags '*' | xargs -0 | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+# Use tr(1) to convert --output=text0 to --output=text for
+# comparison. Also translate newlines to spaces to fail with more
+# noise if they are present as delimiters instead of null
+# characters. This assumes there are no newlines in the data.
+test_begin_subtest "Compare text vs. text0 for threads"
+notmuch search --format=text --output=threads '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=threads '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Compare text vs. text0 for messages"
+notmuch search --format=text --output=messages '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=messages '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Compare text vs. text0 for files"
+notmuch search --format=text --output=files '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=files '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Compare text vs. text0 for tags"
+notmuch search --format=text --output=tags '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=tags '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
index c7cae61eebe707ee163516fb868e08b7b574c6ae..29c0ce6efc39318496711eee6e54e68d39ad4fca 100644 (file)
@@ -3,7 +3,8 @@
 dir := util
 extra_cflags += -I$(srcdir)/$(dir)
 
-libutil_c_srcs := $(dir)/xutil.c $(dir)/error_util.c
+libutil_c_srcs := $(dir)/xutil.c $(dir)/error_util.c $(dir)/hex-escape.c \
+                 $(dir)/string-util.c $(dir)/talloc-extra.c
 
 libutil_modules := $(libutil_c_srcs:.c=.o)
 
index 630d22817539b39c4da66db3f84f647abef6187e..d6e60fc99e4e3dfb0de12fc5ebca09177f8fdea6 100644 (file)
@@ -24,7 +24,7 @@
 
 #include "error_util.h"
 
-int
+void
 _internal_error (const char *format, ...)
 {
     va_list va_args;
@@ -35,7 +35,5 @@ _internal_error (const char *format, ...)
     vfprintf (stderr, format, va_args);
 
     exit (1);
-
-    return 1;
 }
 
index bb158220a3d8d4c8cc09e1f7e9af731135f1f160..17c8727d13b2a743dd3afd9642026f3a73b2b1ea 100644 (file)
 
 #include <talloc.h>
 
+#include "function-attributes.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);
+void
+_internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2) NORETURN_ATTRIBUTE;
 
 /* 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
diff --git a/util/hex-escape.c b/util/hex-escape.c
new file mode 100644 (file)
index 0000000..b7e2e07
--- /dev/null
@@ -0,0 +1,161 @@
+/* hex-escape.c -  Manage encoding and decoding of byte strings into path names
+ *
+ * Copyright (c) 2011 David Bremner
+ *
+ * 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: David Bremner <david@tethera.net>
+ */
+
+#include <assert.h>
+#include <string.h>
+#include <talloc.h>
+#include <ctype.h>
+#include "error_util.h"
+#include "hex-escape.h"
+
+static const size_t default_buf_size = 1024;
+
+static const char *output_charset =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-_@=.,";
+
+static const char escape_char = '%';
+
+static int
+is_output (char c)
+{
+    return (strchr (output_charset, c) != NULL);
+}
+
+static int
+maybe_realloc (void *ctx, size_t needed, char **out, size_t *out_size)
+{
+    if (*out_size < needed) {
+
+       if (*out == NULL)
+           *out = talloc_size (ctx, needed);
+       else
+           *out = talloc_realloc (ctx, *out, char, needed);
+
+       if (*out == NULL)
+           return 0;
+
+       *out_size = needed;
+    }
+    return 1;
+}
+
+hex_status_t
+hex_encode (void *ctx, const char *in, char **out, size_t *out_size)
+{
+
+    const char *p;
+    char *q;
+
+    size_t needed = 1;  /* for the NUL */
+
+    assert (ctx); assert (in); assert (out); assert (out_size);
+
+    for (p = in; *p; p++) {
+       needed += is_output (*p) ? 1 : 3;
+    }
+
+    if (*out == NULL)
+       *out_size = 0;
+
+    if (!maybe_realloc (ctx, needed, out, out_size))
+       return HEX_OUT_OF_MEMORY;
+
+    q = *out;
+    p = in;
+
+    while (*p) {
+       if (is_output (*p)) {
+           *q++ = *p++;
+       } else {
+           sprintf (q, "%%%02x", (unsigned char)*p++);
+           q += 3;
+       }
+    }
+
+    *q = '\0';
+    return HEX_SUCCESS;
+}
+
+/* Hex decode 'in' to 'out'.
+ *
+ * This must succeed for in == out to support hex_decode_inplace().
+ */
+static hex_status_t
+hex_decode_internal (const char *in, unsigned char *out)
+{
+    char buf[3];
+
+    while (*in) {
+       if (*in == escape_char) {
+           char *endp;
+
+           /* This also handles unexpected end-of-string. */
+           if (!isxdigit ((unsigned char) in[1]) ||
+               !isxdigit ((unsigned char) in[2]))
+               return HEX_SYNTAX_ERROR;
+
+           buf[0] = in[1];
+           buf[1] = in[2];
+           buf[2] = '\0';
+
+           *out = strtoul (buf, &endp, 16);
+
+           if (endp != buf + 2)
+               return HEX_SYNTAX_ERROR;
+
+           in += 3;
+           out++;
+       } else {
+           *out++ = *in++;
+       }
+    }
+
+    *out = '\0';
+
+    return HEX_SUCCESS;
+}
+
+hex_status_t
+hex_decode_inplace (char *s)
+{
+    /* A decoded string is never longer than the encoded one, so it is
+     * safe to decode a string onto itself. */
+    return hex_decode_internal (s, (unsigned char *) s);
+}
+
+hex_status_t
+hex_decode (void *ctx, const char *in, char **out, size_t * out_size)
+{
+    const char *p;
+    size_t needed = 1; /* for the NUL */
+
+    assert (ctx); assert (in); assert (out); assert (out_size);
+
+    for (p = in; *p; p++)
+       if ((p[0] == escape_char) && isxdigit (p[1]) && isxdigit (p[2]))
+           needed -= 1;
+       else
+           needed += 1;
+
+    if (!maybe_realloc (ctx, needed, out, out_size))
+       return HEX_OUT_OF_MEMORY;
+
+    return hex_decode_internal (in, (unsigned char *) *out);
+}
diff --git a/util/hex-escape.h b/util/hex-escape.h
new file mode 100644 (file)
index 0000000..5182042
--- /dev/null
@@ -0,0 +1,41 @@
+#ifndef _HEX_ESCAPE_H
+#define _HEX_ESCAPE_H
+
+typedef enum hex_status {
+    HEX_SUCCESS = 0,
+    HEX_SYNTAX_ERROR,
+    HEX_OUT_OF_MEMORY
+} hex_status_t;
+
+/*
+ * The API for hex_encode() and hex_decode() is modelled on that for
+ * getline.
+ *
+ * If 'out' points to a NULL pointer a char array of the appropriate
+ * size is allocated using talloc, and out_size is updated.
+ *
+ * If 'out' points to a non-NULL pointer, it assumed to describe an
+ * existing char array, with the size given in *out_size.  This array
+ * may be resized by talloc_realloc if needed; in this case *out_size
+ * will also be updated.
+ *
+ * Note that it is an error to pass a NULL pointer for any parameter
+ * of these routines.
+ */
+
+hex_status_t
+hex_encode (void *talloc_ctx, const char *in, char **out,
+            size_t *out_size);
+
+hex_status_t
+hex_decode (void *talloc_ctx, const char *in, char **out,
+            size_t *out_size);
+
+/*
+ * Non-allocating hex decode to decode 's' in-place. The length of the
+ * result is always equal to or shorter than the length of the
+ * original.
+ */
+hex_status_t
+hex_decode_inplace (char *s);
+#endif
diff --git a/util/string-util.c b/util/string-util.c
new file mode 100644 (file)
index 0000000..a5622d7
--- /dev/null
@@ -0,0 +1,191 @@
+/* string-util.c -  Extra or enhanced routines for null terminated strings.
+ *
+ * Copyright (c) 2012 Jani Nikula
+ *
+ * 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: Jani Nikula <jani@nikula.org>
+ */
+
+
+#include "string-util.h"
+#include "talloc.h"
+
+#include <ctype.h>
+#include <errno.h>
+
+char *
+strtok_len (char *s, const char *delim, size_t *len)
+{
+    /* skip initial delims */
+    s += strspn (s, delim);
+
+    /* length of token */
+    *len = strcspn (s, delim);
+
+    return *len ? s : NULL;
+}
+
+static int
+is_unquoted_terminator (unsigned char c)
+{
+    return c == 0 || c <= ' ' || c == ')';
+}
+
+int
+make_boolean_term (void *ctx, const char *prefix, const char *term,
+                  char **buf, size_t *len)
+{
+    const char *in;
+    char *out;
+    size_t needed = 3;
+    int need_quoting = 0;
+
+    /* Do we need quoting?  To be paranoid, we quote anything
+     * containing a quote, even though it only matters at the
+     * beginning, and anything containing non-ASCII text. */
+    for (in = term; *in && !need_quoting; in++)
+       if (is_unquoted_terminator (*in) || *in == '"'
+           || (unsigned char)*in > 127)
+           need_quoting = 1;
+
+    if (need_quoting)
+       for (in = term; *in; in++)
+           needed += (*in == '"') ? 2 : 1;
+    else
+       needed = strlen (term) + 1;
+
+    /* Reserve space for the prefix */
+    if (prefix)
+       needed += strlen (prefix) + 1;
+
+    if ((*buf == NULL) || (needed > *len)) {
+       *len = 2 * needed;
+       *buf = talloc_realloc (ctx, *buf, char, *len);
+    }
+
+    if (! *buf) {
+       errno = ENOMEM;
+       return -1;
+    }
+
+    out = *buf;
+
+    /* Copy in the prefix */
+    if (prefix) {
+       strcpy (out, prefix);
+       out += strlen (prefix);
+       *out++ = ':';
+    }
+
+    if (! need_quoting) {
+       strcpy (out, term);
+       return 0;
+    }
+
+    /* Quote term by enclosing it in double quotes and doubling any
+     * internal double quotes. */
+    *out++ = '"';
+    in = term;
+    while (*in) {
+       if (*in == '"')
+           *out++ = '"';
+       *out++ = *in++;
+    }
+    *out++ = '"';
+    *out = '\0';
+
+    return 0;
+}
+
+static const char*
+skip_space (const char *str)
+{
+    while (*str && isspace ((unsigned char) *str))
+       ++str;
+    return str;
+}
+
+int
+parse_boolean_term (void *ctx, const char *str,
+                   char **prefix_out, char **term_out)
+{
+    int err = EINVAL;
+    *prefix_out = *term_out = NULL;
+
+    /* Parse prefix */
+    str = skip_space (str);
+    const char *pos = strchr (str, ':');
+    if (! pos || pos == str)
+       goto FAIL;
+    *prefix_out = talloc_strndup (ctx, str, pos - str);
+    if (! *prefix_out) {
+       err = ENOMEM;
+       goto FAIL;
+    }
+    ++pos;
+
+    /* Implement de-quoting compatible with make_boolean_term. */
+    if (*pos == '"') {
+       char *out = talloc_array (ctx, char, strlen (pos));
+       int closed = 0;
+       if (! out) {
+           err = ENOMEM;
+           goto FAIL;
+       }
+       *term_out = out;
+       /* Skip the opening quote, find the closing quote, and
+        * un-double doubled internal quotes. */
+       for (++pos; *pos; ) {
+           if (*pos == '"') {
+               ++pos;
+               if (*pos != '"') {
+                   /* Found the closing quote. */
+                   closed = 1;
+                   pos = skip_space (pos);
+                   break;
+               }
+           }
+           *out++ = *pos++;
+       }
+       /* Did the term terminate without a closing quote or is there
+        * trailing text after the closing quote? */
+       if (!closed || *pos)
+           goto FAIL;
+       *out = '\0';
+    } else {
+       const char *start = pos;
+       /* Check for text after the boolean term. */
+       while (! is_unquoted_terminator (*pos))
+           ++pos;
+       if (*skip_space (pos)) {
+           err = EINVAL;
+           goto FAIL;
+       }
+       /* No trailing text; dup the string so the caller can free
+        * it. */
+       *term_out = talloc_strndup (ctx, start, pos - start);
+       if (! *term_out) {
+           err = ENOMEM;
+           goto FAIL;
+       }
+    }
+    return 0;
+
+ FAIL:
+    talloc_free (*prefix_out);
+    talloc_free (*term_out);
+    errno = err;
+    return -1;
+}
diff --git a/util/string-util.h b/util/string-util.h
new file mode 100644 (file)
index 0000000..0194607
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef _STRING_UTIL_H
+#define _STRING_UTIL_H
+
+#include <string.h>
+
+/* like strtok(3), but without state, and doesn't modify s.  Return
+ * value is indicated by pointer and length, not null terminator.
+ *
+ * Usage pattern:
+ *
+ * const char *tok = input;
+ * const char *delim = " \t";
+ * size_t tok_len = 0;
+ *
+ * while ((tok = strtok_len (tok + tok_len, delim, &tok_len)) != NULL) {
+ *     // do stuff with string tok of length tok_len
+ * }
+ */
+
+char *strtok_len (char *s, const char *delim, size_t *len);
+
+/* Construct a boolean term query with the specified prefix (e.g.,
+ * "id") and search term, quoting term as necessary.  Specifically, if
+ * term contains any non-printable ASCII characters, non-ASCII
+ * characters, close parenthesis or double quotes, it will be enclosed
+ * in double quotes and any internal double quotes will be doubled
+ * (e.g. a"b -> "a""b").  The result will be a valid notmuch query and
+ * can be parsed by parse_boolean_term.
+ *
+ * Output is into buf; it may be talloc_realloced.
+ * Return: 0 on success, -1 on error.  errno will be set to ENOMEM if
+ * there is an allocation failure.
+ */
+int make_boolean_term (void *talloc_ctx, const char *prefix, const char *term,
+                      char **buf, size_t *len);
+
+/* Parse a boolean term query consisting of a prefix, a colon, and a
+ * term that may be quoted as described for make_boolean_term.  If the
+ * term is not quoted, then it ends at the first whitespace or close
+ * parenthesis.  str may containing leading or trailing whitespace,
+ * but anything else is considered a parse error.  This is compatible
+ * with anything produced by make_boolean_term, and supports a subset
+ * of the quoting styles supported by Xapian (and hence notmuch).
+ * *prefix_out and *term_out will be talloc'd with context ctx.
+ *
+ * Return: 0 on success, -1 on error.  errno will be set to EINVAL if
+ * there is a parse error or ENOMEM if there is an allocation failure.
+ */
+int
+parse_boolean_term (void *ctx, const char *str,
+                   char **prefix_out, char **term_out);
+
+#endif
diff --git a/util/talloc-extra.c b/util/talloc-extra.c
new file mode 100644 (file)
index 0000000..9626247
--- /dev/null
@@ -0,0 +1,14 @@
+#include <string.h>
+#include "talloc-extra.h"
+
+char *
+talloc_strndup_named_const (void *ctx, const char *str,
+                           size_t len, const char *name)
+{
+    char *ptr = talloc_strndup (ctx, str, len);
+
+    if (ptr)
+       talloc_set_name_const (ptr, name);
+
+    return ptr;
+}
diff --git a/util/talloc-extra.h b/util/talloc-extra.h
new file mode 100644 (file)
index 0000000..eac5dc0
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef _TALLOC_EXTRA_H
+#define _TALLOC_EXTRA_H
+
+#include <talloc.h>
+
+/* Like talloc_strndup, but take an extra parameter for the internal talloc
+ * name (for debugging) */
+
+char *
+talloc_strndup_named_const (void *ctx, const char *str,
+                           size_t len, const char *name);
+
+/* use the __location__ macro from talloc.h to name a string according to its
+ * source location */
+
+#define talloc_strndup_debug(ctx, str, len) talloc_strndup_named_const (ctx, str, len, __location__)
+
+#endif
diff --git a/version b/version
index 9beb74d490bcac4ce1c67a584dd97ac4924247ed..e815b861f023432f2500015179faa26fdaba7f9e 100644 (file)
--- a/version
+++ b/version
@@ -1 +1 @@
-0.13.2
+0.15.1