Merge branch 'debian' into rebuild
authorCarl Worth <cworth@cworth.org>
Tue, 6 Apr 2010 21:38:04 +0000 (14:38 -0700)
committerCarl Worth <cworth@cworth.org>
Tue, 6 Apr 2010 21:38:04 +0000 (14:38 -0700)
Conflicts:

  Makefile.local: The Makefiles were all recently re-written on
  master, but I did ensure that the changes from the
  debian branch were all implemented here, (in
  particular, installing the emacs files from "make
  install").

  configure: I've reverted one change as part of this merge:

commit 9f99a301b158dc1ed1c8c6754db1d57e3b0becf4

Remove ./configure failure for unrecognized options

     I'd much rather find what options the Debian scripts pass
     and either implement them or at least make the explicitly
     do nothing. One of the things that often annoyed me about
     gnu autoconf-generated configure scripts was the silent
     ignoring of unknown options, (which was very unhelpful in
     the case of mistyped options on the command line).

57 files changed:
.gitignore
INSTALL
Makefile
Makefile.local
NEWS [new file with mode: 0644]
README
RELEASING [new file with mode: 0644]
TODO
compat/Makefile.local
compat/README [new file with mode: 0644]
compat/have_getline.c [new file with mode: 0644]
completion/Makefile [new file with mode: 0644]
completion/Makefile.local [new file with mode: 0644]
completion/README [new file with mode: 0644]
completion/notmuch-completion.bash [new file with mode: 0644]
completion/notmuch-completion.tcsh [new file with mode: 0644]
completion/notmuch-completion.zsh [new file with mode: 0644]
config/README [deleted file]
config/have_getline.c [deleted file]
configure
contrib/notmuch-completion.bash [deleted file]
contrib/notmuch-completion.tcsh [deleted file]
contrib/notmuch-completion.zsh [deleted file]
emacs/Makefile [new file with mode: 0644]
emacs/Makefile.local [new file with mode: 0644]
emacs/notmuch-lib.el [new file with mode: 0644]
emacs/notmuch-query.el [new file with mode: 0644]
emacs/notmuch-show.el [new file with mode: 0644]
emacs/notmuch.el [new file with mode: 0644]
json.c [new file with mode: 0644]
lib/Makefile.local
lib/database-private.h
lib/database.cc
lib/directory.cc
lib/index.cc
lib/message.cc
lib/messages.c
lib/notmuch-private.h
lib/notmuch.h
lib/query.cc
lib/tags.c
lib/thread.cc
notmuch-client.h
notmuch-dump.c
notmuch-new.c
notmuch-reply.c
notmuch-restore.c
notmuch-search-tags.c
notmuch-search.c
notmuch-show.c
notmuch-tag.c
notmuch.1
notmuch.c
notmuch.desktop
notmuch.el [deleted file]
show-message.c
test/notmuch-test [new file with mode: 0755]

index efa98fbbe03db365f815fa9c1107f792af5172b0..217440d50fd2487094d3d769b1f067052c523e0c 100644 (file)
@@ -1,11 +1,15 @@
+.first-build-message
 Makefile.config
 TAGS
 tags
 *cscope*
 .deps
 notmuch
+notmuch-shared
 notmuch.1.gz
+libnotmuch.so*
 *.[ao]
 *~
 .*.swp
 *.elc
+releases
diff --git a/INSTALL b/INSTALL
index c0b67a319642a41e2ec9b86009a60745cfe71598..bc7bc6778276490279dced12e32c0b39421695e5 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -9,10 +9,14 @@ sequence of:
        make
        sudo make install
 
-You can even skip the configure step if all of the dependencies of
-Notmuch are satisfied. If they are not, the configure script will
-notice that and provide instructions on where to obtain the necessary
-dependencies.
+In fact, if you don't plan to pass any arguments to the configure
+script, then you can skip that step and just start with "make", (which
+will call configure for you). See this command:
+
+       ./configure --help
+
+for detailed documentation of the things you can control at the
+configure stage.
 
 notmuch.el installation
 -----------------------
@@ -64,16 +68,19 @@ which are each described below:
 
        Talloc is available from http://talloc.samba.org/
 
-On a modern, package-based operating system such as Debian, you can
-install all of the dependencies with the following simple command
-line:
+On a modern, package-based operating system you can install all of the
+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
 
-On other systems, a similar command can be used, but the details of
-the package names may be different, (such as "devel" in place of
-"dev").
+  For Fedora and similar:
+
+       sudo yum install xapian-core-devel gmime-devel libtalloc-devel
 
+On other systems, a similar command can be used, but the details of
+the package names may be different.
 
        
 
index 021fdb82a06d60dff275e1d52ad324525b78321c..076efc79b9532c3e1b80a8ef5b9687456a2fe895 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,82 +1,14 @@
-WARN_CXXFLAGS=-Wall -Wextra -Wwrite-strings -Wswitch-enum
-WARN_CFLAGS=$(WARN_CXXFLAGS) -Wmissing-declarations
+# We want the all target to be the implicit target (if no target is
+# given explicitly on the command line) so mention it first.
+all:
 
-# Additional programs that are used during the compilation process.
-EMACS ?= emacs
-# Lowercase to avoid clash with GZIP environment variable for passing
-# arguments to gzip.
-gzip = gzip
+# List all subdirectories here. Each contains its own Makefile.local
+subdirs = compat completion emacs lib
 
-bash_completion_dir = /etc/bash_completion.d
+# We make all targets depend on the Makefiles themselves.
+global_deps = Makefile Makefile.local \
+       $(subdirs:%=%/Makefile) $(subdirs:%=%/Makefile.local)
 
-all_deps = Makefile Makefile.local Makefile.config \
-                  lib/Makefile lib/Makefile.local
-
-extra_cflags :=
-extra_cxxflags :=
-
-# Now smash together user's values with our extra values
-FINAL_CFLAGS = $(CFLAGS) $(WARN_CFLAGS) $(CONFIGURE_CFLAGS) $(extra_cflags)
-FINAL_CXXFLAGS = $(CXXFLAGS) $(WARN_CXXFLAGS) $(CONFIGURE_CXXFLAGS) $(extra_cflags) $(extra_cxxflags)
-FINAL_LDFLAGS = $(LDFLAGS) $(CONFIGURE_LDFLAGS)
-
-all: notmuch notmuch.1.gz
-
-# Before including any other Makefile fragments, get settings from the
-# output of configure
-Makefile.config: configure
-       @echo ""
-       @echo "Note: Calling ./configure with no command-line arguments. This is often fine,"
-       @echo "      but if you want to specify any arguments (such as an alternate prefix"
-       @echo "      into which to install), call ./configure explicitly and then make again."
-       @echo "      See \"./configure --help\" for more details."
-       @echo ""
-       ./configure
-
-include Makefile.config
-
-include lib/Makefile.local
-include compat/Makefile.local
-include Makefile.local
-
-# The user has not set any verbosity, default to quiet mode and inform the
-# user how to enable verbose compiles.
-ifeq ($(V),)
-quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n"
-quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"  $1 $2        $@\n"; $($1)
-endif
-# The user has explicitly enabled quiet compilation.
-ifeq ($(V),0)
-quiet = @printf "  $1  $@\n"; $($1)
-endif
-# Otherwise, print the full command line.
-quiet ?= $($1)
-
-%.o: %.cc $(all_deps)
-       $(call quiet,CXX,$(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@
-
-%.o: %.c $(all_deps)
-       $(call quiet,CC,$(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@
-
-%.elc: %.el
-       $(call quiet,EMACS) -batch -f batch-byte-compile $<
-
-.deps/%.d: %.c $(all_deps)
-       @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
-       $(CC) -M $(CPPFLAGS) $(FINAL_CFLAGS) $< > $@.$$$$ 2>/dev/null ; \
-       sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
-       rm -f $@.$$$$
-
-.deps/%.d: %.cc $(all_deps)
-       @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
-       $(CXX) -M $(CPPFLAGS) $(FINAL_CXXFLAGS) $< > $@.$$$$ 2>/dev/null ; \
-       sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
-       rm -f $@.$$$$
-
-DEPS := $(SRCS:%.c=.deps/%.d)
-DEPS := $(DEPS:%.cc=.deps/%.d)
--include $(DEPS)
-
-.PHONY : clean
-clean:
-       rm -f $(CLEAN); rm -rf .deps
+# Finally, include all of the Makefile.local fragments where all the
+# real work is done.
+include Makefile.local $(subdirs:%=%/Makefile.local)
index 2d5409659ec3e1930e40a37d41020b05c154ebf6..16b0103b8599f7fdedd2ca8a42fdcdd60b4e0251 100644 (file)
@@ -1,4 +1,142 @@
-emacs: notmuch.elc
+# -*- makefile -*-
+
+# Here's the (hopefully simple) versioning scheme.
+#
+# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We
+# increment the second digit for each release and increment the first
+# digit when we reach particularly major milestones of usability.
+#
+# Between releases, (such as when compiling notmuch from the git
+# repository), we add a third digit, (0.1.1, 0.1.2, etc.), and
+# increment it occasionally, (such as after a big batch of commits are
+# merged.
+PACKAGE=notmuch
+VERSION=0.1
+
+RELEASE_HOST=notmuchmail.org
+RELEASE_DIR=/srv/notmuchmail.org/www/releases
+TAR_FILE=$(PACKAGE)-$(VERSION).tar.gz
+SHA1_FILE=$(TAR_FILE).sha1
+GPG_FILE=$(SHA1_FILE).asc
+
+# Get settings from the output of configure by running it to generate
+# Makefile.config if it doesn't exist yet. And add Makefile.config to
+# our global dependency list.
+include Makefile.config
+global_deps += Makefile.config
+Makefile.config: configure
+       @echo ""
+       @echo "Note: Calling ./configure with no command-line arguments. This is often fine,"
+       @echo "      but if you want to specify any arguments (such as an alternate prefix"
+       @echo "      into which to install), call ./configure explicitly and then make again."
+       @echo "      See \"./configure --help\" for more details."
+       @echo ""
+       ./configure
+
+# Sub-directory Makefile.local fragments can append to these variables
+# to have directory-specific cflags as necessary.
+extra_cflags :=
+extra_cxxflags :=
+
+# Smash together user's values with our extra values
+FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CFLAGS) $(WARN_CFLAGS) $(CONFIGURE_CFLAGS) $(extra_cflags)
+FINAL_CXXFLAGS = $(CXXFLAGS) $(WARN_CXXFLAGS) $(CONFIGURE_CXXFLAGS) $(extra_cflags) $(extra_cxxflags)
+FINAL_LDFLAGS = $(LDFLAGS) $(CONFIGURE_LDFLAGS)
+
+.PHONY: all
+all: notmuch notmuch-shared notmuch.1.gz
+ifeq ($(MAKECMDGOALS),)
+ifeq ($(shell cat .first-build-message),)
+       @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
+       @echo ""
+       @echo "Compilation of notmuch is now complete. You can install notmuch with:"
+       @echo ""
+       @echo " make install"
+       @echo ""
+       @echo "Note that depending on the prefix to which you are installing"
+       @echo "you may need root permission (such as \"sudo make install\")."
+       @echo "See \"./configure --help\" for help on setting an alternate prefix."
+       @echo Printed > .first-build-message
+endif
+endif
+
+$(TAR_FILE):
+       git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ HEAD | gzip > $(TAR_FILE)
+       @echo "Source is ready for release in $(TAR_FILE)"
+
+$(SHA1_FILE): $(TAR_FILE)
+       sha1sum $^ > $@
+
+$(GPG_FILE): $(SHA1_FILE)
+       @echo "Please enter your GPG password to sign the checksum."
+       gpg --armor --sign $^ 
+
+.PHONY: dist
+dist: $(TAR_FILE)
+
+.PHONY: release
+release: release-verify-newer $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE)
+       mkdir -p releases
+       scp $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) $(RELEASE_HOST):$(RELEASE_DIR)
+       mv $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) releases
+       ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-[0-9]* && ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(PACKAGE)-$(VERSION)"
+       git tag -s -m "$(PACKAGE) $(VERSION) release" $(VERSION)
+
+.PHONY: release-verify-version
+release-verify-version:
+       @echo -n "Checking that $(VERSION) is a two-component version..."
+       @if echo $(VERSION) | grep -q -v -x '[0-9]*\.[0-9]*'; then \
+               (echo "Ouch." && \
+                echo "Before releasing the notmuch version should be a two-component value." && false);\
+        else :; fi
+       @echo "Good."
+
+.PHONY: release-verify-newer
+release-verify-newer: release-verify-version
+       @echo -n "Checking that no $(VERSION) release already exists..."
+       @ssh $(RELEASE_HOST) test ! -e $(RELEASE_DIR)/$(TAR_FILE) \
+               || (echo "Ouch." && echo "Found: $(RELEASE_HOST):$(RELEASE_DIR)/$(TAR_FILE)" \
+               && echo "Refusing to replace an existing release." && false)
+       @echo "Good."
+
+# The user has not set any verbosity, default to quiet mode and inform the
+# user how to enable verbose compiles.
+ifeq ($(V),)
+quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n"
+quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//'))
+endif
+# The user has explicitly enabled quiet compilation.
+ifeq ($(V),0)
+quiet = @printf "$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//'))
+endif
+# Otherwise, print the full command line.
+quiet ?= $($(shell echo $1 | sed -e s'/ .*//'))
+
+%.o: %.cc $(global_deps)
+       $(call quiet,CXX $(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@
+
+%.o: %.c $(global_deps)
+       $(call quiet,CC $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@
+
+.deps/%.d: %.c $(global_deps)
+       @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
+       $(CC) -M $(CPPFLAGS) $(FINAL_CFLAGS) $< > $@.$$$$ 2>/dev/null ; \
+       sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
+       rm -f $@.$$$$
+
+.deps/%.d: %.cc $(global_deps)
+       @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
+       $(CXX) -M $(CPPFLAGS) $(FINAL_CXXFLAGS) $< > $@.$$$$ 2>/dev/null ; \
+       sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
+       rm -f $@.$$$$
+
+DEPS := $(SRCS:%.c=.deps/%.d)
+DEPS := $(DEPS:%.cc=.deps/%.d)
+-include $(DEPS)
+
+.PHONY : clean
+clean:
+       rm -f $(CLEAN); rm -rf .deps
 
 notmuch_client_srcs =          \
        $(notmuch_compat_srcs)  \
@@ -18,35 +156,41 @@ notmuch_client_srcs =              \
        notmuch-tag.c           \
        notmuch-time.c          \
        query-string.c          \
-       show-message.c
+       show-message.c          \
+       json.c
 
 notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
-notmuch: $(notmuch_client_modules) lib/notmuch.a
-       $(call quiet,CXX,$(LDFLAGS)) $^ $(FINAL_LDFLAGS) -o $@
+
+notmuch: $(notmuch_client_modules) lib/libnotmuch.a
+       $(call quiet,CC $(CFLAGS)) $^ $(FINAL_LDFLAGS) -o $@
+
+notmuch-shared: $(notmuch_client_modules) lib/libnotmuch.so
+       $(call quiet,CC $(CFLAGS)) -Llib -lnotmuch $(notmuch_client_modules) $(FINAL_LDFLAGS) -o $@
 
 notmuch.1.gz: notmuch.1
-       $(call quiet,gzip) --stdout $^ > $@
-
-install: all notmuch.1.gz install-emacs install-bash
-       for d in $(DESTDIR)$(prefix)/bin/ $(DESTDIR)$(prefix)/share/man/man1 ; \
-       do \
-               install -d $$d ; \
-       done ;
-       install notmuch $(DESTDIR)$(prefix)/bin/
-       install -m0644 notmuch.1.gz $(DESTDIR)$(prefix)/share/man/man1/
+       gzip --stdout $^ > $@
 
-install-emacs: emacs
-       for d in $(DESTDIR)/$(emacs_lispdir) ; \
-       do \
-               install -d $$d ; \
-       done ;
-       install -m0644 notmuch.el $(DESTDIR)$(emacs_lispdir)
-       install -m0644 notmuch.elc $(DESTDIR)$(emacs_lispdir)
+.PHONY: install
+install: all notmuch.1.gz
+       mkdir -p $(DESTDIR)$(prefix)/share/man/man1
+       install -m0644 notmuch.1.gz $(DESTDIR)$(prefix)/share/man/man1/
+       mkdir -p $(DESTDIR)$(prefix)/bin/
+       install notmuch-shared $(DESTDIR)$(prefix)/bin/notmuch
+ifeq ($(MAKECMDGOALS), install)
+       @echo ""
+       @echo "Notmuch is now installed to $(DESTDIR)$(prefix)"
+       @echo ""
+       @echo "To run notmuch from emacs, each user should add the following line to ~/.emacs:"
+       @echo ""
+       @echo " (require 'notmuch)"
+       @echo ""
+       @echo "And should then run \"M-x notmuch\" from within emacs or run \"emacs -f notmuch\""
+endif
 
-install-bash:
-       install -d $(DESTDIR)$(bash_completion_dir)
-       install -m0644 contrib/notmuch-completion.bash \
-               $(DESTDIR)$(bash_completion_dir)/notmuch
+.PHONY: install-desktop
+install-desktop:
+       mkdir -p $(DESTDIR)$(desktop_dir)
+       desktop-file-install --mode 0644 --dir $(DESTDIR)$(desktop_dir) notmuch.desktop
 
 SRCS  := $(SRCS) $(notmuch_client_srcs)
-CLEAN := $(CLEAN) notmuch $(notmuch_client_modules) notmuch.elc notmuch.1.gz
+CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) notmuch.elc notmuch.1.gz
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..f29ac27
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,13 @@
+Notmuch 0.1 (2010-04-05)
+========================
+This is the first release of the notmuch mail system.
+
+It includes the libnotmuch library, the notmuch command-line
+interface, and an emacs-based interface to notmuch.
+
+Note: Notmuch will work best with Xapian 1.0.18 (or later) or Xapian
+1.1.4 (or later). Previous versions of Xapian (whether 1.0 or 1.1) had
+a performance bug that made notmuch very slow when modifying
+tags. This would cause distracting pauses when reading mail while
+notmuch would wait for Xapian when removing the "inbox" and "unread"
+tags from messages in a thread.
diff --git a/README b/README
index e47dd4206818af55e505f5562b20f3643491af33..5f029c828280f6661ea045d2a06496214686cc82 100644 (file)
--- a/README
+++ b/README
@@ -1,9 +1,9 @@
 Notmuch - thread-based email index, search and tagging.
 
 Notmuch is a system for indexing, searching, reading, and tagging
-large collections of email messages. It uses the Xapian library to
-provide fast, full-text search of very large collection of email with
-a very convenient search syntax.
+large collections of email messages in maildir or mh format. It uses
+the Xapian library to provide fast, full-text search with a convenient
+search syntax.
 
 Notmuch is free software, released under the GNU General Public
 License version 3 (or later).
diff --git a/RELEASING b/RELEASING
new file mode 100644 (file)
index 0000000..3a1c6dd
--- /dev/null
+++ b/RELEASING
@@ -0,0 +1,71 @@
+Here are the steps to follow to create a new notmuch release:
+
+1) Verify that what you want to release is committed. The release
+   process will release the code from the current HEAD commit.
+
+2) Verify that the NEWS file is up to date.
+
+       Read through the entry at the top of the NEWS file and see if
+       you are aware of any major features recently added that are
+       not mentioned there. If so, pleas add them, (and ask the
+       authors of the commits to update NEWS in the future).
+
+3) Verify that the notmuch test suite passes.
+
+       Currently this is by running:
+
+               ./test/notmuch-test
+
+       And manually verifying that every test says PASS. We plan to
+       fix this to automatically check the results and even to
+       automatically run the test suite as part of a Makefile target
+       described below.
+
+4) Increment the libnotmuch library version in lib/Makefile.local
+
+       See the instructions there for how to increment it. The
+       command below can be useful for inspecting header-file changes
+       since the last release X.Y.Z:
+
+               git diff X.Y.Z..HEAD -- lib/notmuch.h
+
+       Note: We currently don't plan to increment
+       LIBNOTMUCH_VERSION_MAJOR beyond 1, so if there *are*
+       incompatible changes to the library interface, then
+       stop. Don't release. Figure out the plan on the notmuch
+       mailing list.
+
+       Commit this change.
+
+5) Increment the notmuch version in Makefile.local
+
+       For most releases we'll just increment the minor number. For
+       major milestones of usability we'll increment the major
+       number.
+
+       Commit this change.
+
+6) Run "make release" which will perform the following steps:
+
+       * Check that the notmuch version consists of only two components
+       * Check that no release exists with the current version
+       * Verify that "make dist" completes successfully
+       * Generate the final tar file
+       * Generate an sha1sum file
+       * Sign the sha1sum using your GPG setup (asks for your GPG password)
+       * scp the three files to appear on http://notmuchmail.org/releases
+       * Place local copies of the three files in the releases directory
+       * Create a LATEST-notmuch-version file (after deleting any old one)
+       * Tag the entire source tree with a tag of the form X.Y.Z, and sign
+         the tag with your GPG key (asks for your GPG password, and you
+         may need to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL to match
+         your public-key's setting or this fails.)
+       * Provide some text for the release announcement (see below).
+         If for some reason you lost this message, "make release-publish-message"
+         prints it for you.
+
+7) Increment the notmuch version by adding a .1 micro number, commit, and push.
+
+8) Send a message to notmuch@notmuchmail.org to announce the release.
+
+       Use the text from the new entry to NEWS.
diff --git a/TODO b/TODO
index bdfe64c673448a7baadf494524fa6fe116eca4e7..572dac8ee3e3709d5a78fb7ddb496a04950bc6bc 100644 (file)
--- a/TODO
+++ b/TODO
@@ -4,8 +4,6 @@ Fix the things that are causing the most pain to new users
 
 2. Allow an easy way to get tags from directory names (if the user has them)
 
-3. Fix Xapian defect #250 so tagging is fast.
-
 Emacs interface (notmuch.el)
 ----------------------------
 Enhance '+' and '-' in the search view to operate on an entire region
@@ -16,14 +14,9 @@ the entire buffer.
 
 Add a global keybinding table for notmuch, and then view-specific
 tables that add to it.
-
-Add a command to archive all threads in a search view.
        
 Add a '|' binding from the search view.
 
-When a thread has been entirely read, start out by closing all
-messages except those that matched the search terms.
-
 Add support for choosing from one of the user's configured email
 addresses for the From line.
 
@@ -38,6 +31,44 @@ Add support to "mute" a thread (add a "muted" tag and then don't
 display threads in searches by default where any message of the thread
 has the "muted" tag).
 
+Fix i-search to open up invisible citations as necessary.
+
+Make '=' count from the end rather than from the beginning if more
+than half-way through the buffer.
+
+Emacs saved-search interface
+----------------------------
+Here's a proposal Carl wrote (id:87einafy4u.fsf@yoom.home.cworth.org):
+
+  So what I'm imagining for the default notmuch view is something like
+  this:
+
+          Welcome to notmuch.
+
+              Notmuch search: _________________________________________
+
+          Saved searches:
+
+              55,342      All messages
+                  22      Inbox
+
+          Recent searches:
+
+                   1      from:"someone special" and tag:unread
+                  34      tag:notmuch and tag:todo
+
+          Click (or press Enter) on any search to see the results.
+          Right-click (or press Space) on any recent search to save it.
+
+  So the "saved searches" portion of the view is basically just what
+  notmuch-folder displays now. Above that there's an obvious place to
+  start a new search, (in a slightly more "web-browser-like" way than the
+  typical mini-buffer approach).
+
+  All recent searches appear in the list at the bottom automatically, and
+  there's the documented mechanism for saving a search, (giving it a name
+  and having it appear above).
+
 Portability
 -----------
 Fix configure script to test each compiler warning we want to use.
@@ -50,6 +81,8 @@ and *then* --max-threads), and also complete value for --sort=
 
 notmuch command-line tool
 -------------------------
+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).
@@ -57,16 +90,18 @@ filtering that the user wants to do).
 Fix "notmuch show" so that the UI doesn't fail to show a thread that
 is visible in a search buffer, but happens to no longer match the
 current search. (Perhaps add a --matching=<secondary-search-terms>
-option (or similar) to "notmuch show".)
+option (or similar) to "notmuch show".) For now, this is being worked
+around in the emacs interface by noticing that "notmuch show" returns
+nothing and re-rerunning the command without the extra arguments.
 
 Teach "notmuch search" to return many different kinds of results. Some
 ideas:
 
-       notmuch search --for threads    # Default if no --for is given
-       notmuch search --for messages
-       notmuch search --for tags
-       notmuch search --for addresses
-       notmuch search --for terms
+       notmuch search --output=threads # Default if no --output is given
+       notmuch search --output=messages
+       notmuch search --output=tags
+       notmuch search --output=addresses
+       notmuch search --output=terms
 
 Add a "--format" option to "notmuch search", (something printf-like
 for selecting what gets printed).
@@ -74,11 +109,7 @@ for selecting what gets printed).
 Add a "--count-only" (or so?) option to "notmuch search" for returning
 the count of search results.
 
-Add documented syntax for searching all threads/messages.
-
-Give "notmuch restore" some progress indicator. Until we get the
-Xapian bugs fixed that are making this operation slow, we really need
-to let the user know that things are still moving.
+Give "notmuch restore" some progress indicator.
 
 Fix "notmuch restore" to operate in a single pass much like "notmuch
 dump" does, rather than doing N searches into the database, each
@@ -87,32 +118,20 @@ matching 1/N messages.
 Add a "-f <filename>" option to select an alternate configuration
 file.
 
-Fix notmuch.c to call add_timestamp/get_timestamp with path names
-relative to the database path. (Otherwise, moving the database to a
-new directory will result in notmuch creating new timestamp documents
-and leaving stale ones behind.)
-
-Fix notmuch.c to use a DIR prefix for directory timestamps, (the idea
-being that it can then add other non-directory timestamps such as for
-noting how far back in the past mail has been indexed, and whether it
-needs to re-tag messages based on a theoretical "auto-tags"
-configuration file).
-
-Make "notmuch new" notice when a mail directory has gone more than a
-month without receiving new mail and use that to trigger the printing
-of the note that the user might want to mark the directory read-only.
-
-Also make "notmuch new" optionally able to just mark those month-old
-directories read-only on its own. (Could conflict with low-volume
-lists such as announce lists if they are setup to deliver to their own
-maildirs.)
-
 Allow configuration for filename patterns that should be ignored when
 indexing.
 
+Replace the "notmuch part --part=id" command with "notmuch show
+--part=id", (David Edmonson wants to rewrite some of "notmuch show" to
+provide more MIME-structure information in its output first).
+
+Replace the "notmuch search-tags" command with "notmuch search
+--output=tags".
+
 notmuch library
 ---------------
-Index content from citations, please.
+Add an interface to accept a "key" and a byte stream, rather than a
+filename.
 
 Provide a sane syntax for date ranges. First, we don't want to require
 both endpoints to be specified. For example it would be nice to be
@@ -127,9 +146,6 @@ Make failure to read a file (such as a permissions problem) a warning
 rather than an error (should be similar to the existing warning for a
 non-mail file).
 
-Add support for files that are moved or deleted (which obviously need
-to be handled differently).
-
 Actually compile and install a libnotmuch shared library.
 
 Fix to use the *last* Message-ID header if multiple such headers are
@@ -152,60 +168,40 @@ Fix notmuch_query_count_messages to share code with
 notmuch_query_search_messages rather than duplicating code. (And
 consider renaming it as well.)
 
-General
--------
-Audit everything for dealing with out-of-memory (and drop xutil.c).
-
-Write a test suite.
-
+Provide a mechanism for doing automatic address completion based on
+notmuch searches. Here was one proposal made in IRC:
+
+       <cworth> I guess all it would really have to be would be a way
+                to configure a series of searches to try in turn,
+                (presenting ambiguities at a given single level, and
+                advancing to the next level only if one level
+                returned no matches).
+       <cworth> So then I might have a series that looks like this:
+       <cworth> notmuch search --output=address_from tag:address_book_alias
+       <cworth> notmuch search --output=address_to tag:sent
+       <cworth> notmuch search --output=address_from
+       <cworth> I think I might like that quite a bit.
+       <cworth> And then we have a story for an address book for
+                non-emacs users.
+
+Provide a ~me Xapian synonym for all of the user's configured email
+addresses.
+
+Test suite
+----------
 Achieve 100% test coverage with the test suite.
 
-Investigate why the notmuch database is slightly larger than the sup
-database for the same corpus of email.
-
-Xapian
-------
-Fix defect #250
-
-       replace_document should make minimal changes to database file
-       http://trac.xapian.org/ticket/250
-
-       It looks like it's going to be easy to fix. Here's the file to
-       change:
+Modularize test suite (to be able to run individual tests).
 
-               xapian-core/backends/flint/flint_database.cc
+Summarize test results at the end.
 
-       And look for:
+Fix the insane quoting nightmare of the test suite, (and once we do
+that we can actually test the implicit-phrase search feature such as
+"notmuch search 'body search (phrase)'"
 
-         // FIXME - in the case where there is overlap between the new
-         // termlist and the old termlist, it would be better to compare the
-         // two lists, and make the minimum set of modifications required.
-         // This would lead to smaller changesets for replication, and
-         // probably be faster overall
-
-       So I think this might be as easy as just walking over two
-       sorted lists looking for differences.
-
-       Note that this is in the currently default "flint" backend,
-       but the Xapian folks are probably more interested in fixing
-       the in-development "chert" backend. So the patch to get
-       upstreamed there will probably also fix:
-
-               xapian-core/backends/chert/chert_database.cc
-
-       (I'm hoping the fix will be the same---an identical comment
-       exists there.)
-
-       Also, if you want to experiment with the chert backend,
-       compile current Xapian source and run notmuch with
-       XAPIAN_PREFER_CHERT=1. I haven't tried that yet, but there are
-       claims that a chert database can be 40% smaller than an
-       equivalent flint database.
-
-Report this bug:
-
-       "tag:foo and tag:bar and -tag:deleted" goes insane
+General
+-------
+Audit everything for dealing with out-of-memory (and drop xutil.c).
 
-       This seems to be triggered by a Boolean operator next to a
-       token starting with a non-word character---suddenly all the
-       Boolean operators get treated as literal tokens)
+Investigate why the notmuch database is slightly larger than the sup
+database for the same corpus of email.
index ccc59aefdf2517e1db94ca91c7ac0c55a23111d5..81e6c707d6ad9f2e1ff2c4a3b14aa9829eaa242f 100644 (file)
@@ -1,4 +1,6 @@
-dir=compat
+# -*- makefile -*-
+
+dir := compat
 extra_cflags += -I$(dir)
 
 notmuch_compat_srcs =
diff --git a/compat/README b/compat/README
new file mode 100644 (file)
index 0000000..cd32c56
--- /dev/null
@@ -0,0 +1,16 @@
+notmuch/comapt
+
+This directory consists of two things:
+
+1. Small programs used by the notmuch configure script to test for the
+   availability of certain system features, (library functions, etc.).
+
+   For example: have_getline.c
+
+2. Compatibility implementations of those system features for systems
+   that don't provide their own versions.
+
+   For example: getline.c
+
+   The compilation of these files is made conditional on the output of
+   the test programs from [1].
diff --git a/compat/have_getline.c b/compat/have_getline.c
new file mode 100644 (file)
index 0000000..a8bcd17
--- /dev/null
@@ -0,0 +1,13 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <sys/types.h>
+
+int main()
+{
+    ssize_t count = 0;
+    size_t n = 0;
+    char **lineptr = NULL;
+    FILE *stream = NULL;
+
+    count = getline(lineptr, &n, stream);
+}
diff --git a/completion/Makefile b/completion/Makefile
new file mode 100644 (file)
index 0000000..b6859ea
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makfefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/completion/Makefile.local b/completion/Makefile.local
new file mode 100644 (file)
index 0000000..6a6012d
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- makefile -*-
+
+dir := completion
+
+# The dir variable will be re-assigned to later, so we can't use it
+# directly in any shell commands. Instead we save its value in other,
+# private variables that we can use in the commands.
+bash_script := $(dir)/notmuch-completion.bash
+zsh_script := $(dir)/notmuch-completion.zsh
+
+install: install-$(dir)
+
+install-$(dir):
+       @echo $@
+       mkdir -p $(DESTDIR)$(bash_completion_dir)
+       install -m0644 $(bash_script) $(DESTDIR)$(bash_completion_dir)/notmuch
+       mkdir -p $(DESTDIR)$(zsh_completion_dir)
+       install -m0644 $(zsh_script) $(DESTDIR)$(zsh_completion_dir)/notmuch
diff --git a/completion/README b/completion/README
new file mode 100644 (file)
index 0000000..40a30e5
--- /dev/null
@@ -0,0 +1,10 @@
+notmuch completion
+
+This directory contains support for various shells to automatically
+complete partially entered notmuch command lines.
+
+notmuch-completion.bash        Command-line completion for the bash shell
+
+notmuch-completion.tcsh        Command-line completion for the tcsh shell
+
+notmuch-completion.zsh Command-line completion for the zsh shell
diff --git a/completion/notmuch-completion.bash b/completion/notmuch-completion.bash
new file mode 100644 (file)
index 0000000..8665268
--- /dev/null
@@ -0,0 +1,71 @@
+# Bash completion for notmuch
+#
+# Copyright © 2009 Carl Worth
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see http://www.gnu.org/licenses/ .
+#
+# Author: Carl Worth <cworth@cworth.org>
+#
+# Based on "notmuch help" as follows:
+#
+# Usage: notmuch <command> [args...]
+#
+# Where <command> and [args...] are as follows:
+#
+#      setup
+#
+#      new
+#
+#      search [options] <search-term> [...]
+#
+#      show <search-terms>
+#
+#      reply <search-terms>
+#
+#      tag +<tag>|-<tag> [...] [--] <search-terms> [...]
+#
+#      dump [<filename>]
+#
+#      restore <filename>
+#
+#      help [<command>]
+
+_notmuch()
+{
+    local current previous commands help_options
+
+    previous=${COMP_WORDS[COMP_CWORD-1]}
+    current="${COMP_WORDS[COMP_CWORD]}"
+
+    commands="setup new search show reply tag dump restore help"
+    help_options="setup new search show reply tag dump restore search-terms"
+    search_options="--max-threads= --first= --sort="
+
+    COMPREPLY=()
+
+    case $COMP_CWORD in
+        1)
+            COMPREPLY=( $(compgen -W "${commands}" -- ${current}) ) ;;
+        2)
+            case $previous in
+                help)
+                    COMPREPLY=( $(compgen -W "${help_options}" -- ${current}) ) ;;
+                search)
+                    COMPREPLY=( $(compgen -W "${search_options}" -- ${current}) ) ;;
+            esac
+            ;;
+    esac
+}
+
+complete -o default -o bashdefault -F _notmuch notmuch
diff --git a/completion/notmuch-completion.tcsh b/completion/notmuch-completion.tcsh
new file mode 100644 (file)
index 0000000..c0d3a44
--- /dev/null
@@ -0,0 +1,2 @@
+set NOTMUCH_CMD=`notmuch help | awk '/\t/' | cut -f2 |grep -v '^$'`
+complete notmuch 'p/1/$NOTMUCH_CMD/'
diff --git a/completion/notmuch-completion.zsh b/completion/notmuch-completion.zsh
new file mode 100644 (file)
index 0000000..67a9aba
--- /dev/null
@@ -0,0 +1,74 @@
+#compdef notmuch
+
+# ZSH completion for `notmuch`
+# Copyright © 2009 Ingmar Vanhassel <ingmar@exherbo.org>
+
+_notmuch_commands()
+{
+  local -a notmuch_commands
+  notmuch_commands=(
+    'setup:interactively set up notmuch for first use'
+    'new:find and import any new message to the database'
+    'search:search for messages matching the search terms, display matching threads as results'
+    'reply:constructs a reply template for a set of messages'
+    'show:show all messages matching the search terms'
+    'tag:add or remove tags for all messages matching the search terms'
+    'dump:creates a plain-text dump of the tags of each message'
+    'restore:restores the tags from the given file'
+    'help:show details on a command'
+  )
+
+  _describe -t command 'command' notmuch_commands
+}
+
+_notmuch_dump()
+{
+  _files
+}
+
+_notmuch_help_topics()
+{
+  local -a notmuch_help_topics
+  notmuch_help_topics=(
+    'search-terms:show common search-terms syntax'
+  )
+  _describe -t notmuch-help-topics 'topic' notmuch_help_topics
+}
+
+_notmuch_help()
+{
+  _alternative \
+    _notmuch_commands \
+    _notmuch_help_topics
+}
+
+_notmuch_restore()
+{
+  _files
+}
+
+_notmuch_search()
+{
+  _arguments -s : \
+    '--max-threads=[display only the first x threads from the search results]:number of threads to show: ' \
+    '--first=[omit the first x threads from the search results]:number of threads to omit: ' \
+    '--sort=[sort results]:sorting:((newest-first\:"reverse chronological order" oldest-first\:"chronological order"))'
+}
+
+_notmuch()
+{
+  if (( CURRENT > 2 )) ; then
+    local cmd=${words[2]}
+    curcontext="${curcontext%:*:*}:notmuch-$cmd"
+    (( CURRENT-- ))
+    shift words
+    _call_function ret _notmuch_$cmd
+    return ret
+  else
+    _notmuch_commands
+  fi
+}
+
+_notmuch "$@"
+
+# vim: set sw=2 sts=2 ts=2 et ft=zsh :
diff --git a/config/README b/config/README
deleted file mode 100644 (file)
index eabfe28..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-notmuch/config
-
-This directory consists of small programs used by the notmuch
-configure script to test for the availability of certain system
-features, (library functions, etc.).
diff --git a/config/have_getline.c b/config/have_getline.c
deleted file mode 100644 (file)
index a8bcd17..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-#define _GNU_SOURCE
-#include <stdio.h>
-#include <sys/types.h>
-
-int main()
-{
-    ssize_t count = 0;
-    size_t n = 0;
-    char **lineptr = NULL;
-    FILE *stream = NULL;
-
-    count = getline(lineptr, &n, stream);
-}
index a64f3a0183d2dc893599a3fa1fbb09ba74430db3..648d8771b9198ca02f2fb995c66a461ba336aeff 100755 (executable)
--- a/configure
+++ b/configure
@@ -8,9 +8,17 @@ CFLAGS=${CFLAGS:--O2}
 CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)}
 XAPIAN_CONFIG=${XAPIAN_CONFIG:-xapian-config-1.1 xapian-config}
 
+# We don't allow the EMACS or GZIP Makefile variables inherit values
+# from the environment as we do with CC and CXX above. The reason is
+# that these names as environment variables have existing uses other
+# than the program name that we want. (EMACS is set to 't' when a
+# shell is running within emacs and GZIP specifies arguments to pass
+# on the gzip command line).
+
 # Set the defaults for values the user can specify with command-line
 # options.
 PREFIX=/usr/local
+LIBDIR=
 
 usage ()
 {
@@ -51,12 +59,16 @@ command line.
        --prefix=PREFIX Install files in PREFIX [$PREFIX]
 
 By default, "make install" will install the resulting program to
-$PREFIX/bin, documentation to $PREFIX/share, etc. You can
+$PREFIX/bin, documentation to $PREFIX/man, etc. You can
 specify an installation prefix other than $PREFIX using
 --prefix, for instance:
 
        ./configure --prefix=\$HOME
 
+Fine tuning of some installation directories is available:
+
+       --libdir=DIR    Install libraries in LIBDIR [PREFIX/lib]
+
 EOF
 }
 
@@ -67,6 +79,14 @@ for option; do
        exit 0
     elif [ "${option%%=*}" = '--prefix' ] ; then
        PREFIX="${option#*=}"
+    elif [ "${option%%=*}" = '--libdir' ] ; then
+       LIBDIR="${option#*=}"
+    else
+       echo "Unrecognized option: ${option}."
+       echo "See:"
+       echo "  $0 --help"
+       echo ""
+       exit 1
     fi
 done
 
@@ -155,6 +175,15 @@ else
     emacs_lispdir='$(prefix)/share/emacs/site-lisp'
 fi
 
+printf "Checking if emacs is available... "
+if emacs --quick --batch > /dev/null 2>&1; then
+    printf "Yes.\n"
+    have_emacs=1
+else
+    printf "No (so will not byte-compile emacs code)\n"
+    have_emacs=0
+fi
+
 if [ $errors -gt 0 ]; then
     cat <<EOF
 
@@ -221,7 +250,7 @@ EOF
 fi
 
 printf "Checking for getline... "
-if ${CC} -o config/have_getline config/have_getline.c > /dev/null 2>&1
+if ${CC} -o compat/have_getline compat/have_getline.c > /dev/null 2>&1
 then
     printf "Yes.\n"
     have_getline=1
@@ -229,7 +258,7 @@ else
     printf "No (will use our own instead).\n"
     have_getline=0
 fi
-rm -f config/have_getline
+rm -f compat/have_getline
 
 cat <<EOF
 
@@ -256,18 +285,42 @@ CC = ${CC}
 # The C++ compiler to use
 CXX = ${CXX}
 
+# Command to execute emacs from Makefiles
+EMACS = emacs --quick
+
 # Default FLAGS for C compiler (can be overridden by user such as "make CFLAGS=-g")
 CFLAGS = ${CFLAGS}
 
 # Default FLAGS for C++ compiler (can be overridden by user such as "make CXXFLAGS=-g")
 CXXFLAGS = ${CXXFLAGS}
 
+# Flags to enable warnings when using the C++ compiler
+WARN_CXXFLAGS=-Wall -Wextra -Wwrite-strings -Wswitch-enum
+
+# Flags to enable warnings when using the C compiler
+WARN_CFLAGS=\$(WARN_CXXFLAGS) -Wmissing-declarations
+
 # The prefix to which notmuch should be installed
 prefix = ${PREFIX}
 
+# The directory to which notmuch libraries should be installed
+libdir = ${LIBDIR:=\$(prefix)/lib}
+
 # The directory to which emacs lisp files should be installed
 emacs_lispdir=${emacs_lispdir}
 
+# Whether there's an emacs binary available for byte-compiling
+HAVE_EMACS = ${have_emacs}
+
+# The directory to which desktop files should be installed
+desktop_dir = \$(prefix)/share/applications
+
+# The directory to which bash completions files should be installed
+bash_completion_dir = /etc/bash_completion.d
+
+# The directory to which zsh completions files should be installed
+zsh_completion_dir = \$(prefix)/share/zsh/functions/Completion/Unix
+
 # Whether the getline function is available (if not, then notmuch will
 # build its own version)
 HAVE_GETLINE = ${have_getline}
diff --git a/contrib/notmuch-completion.bash b/contrib/notmuch-completion.bash
deleted file mode 100644 (file)
index 8665268..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-# Bash completion for notmuch
-#
-# Copyright © 2009 Carl Worth
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see http://www.gnu.org/licenses/ .
-#
-# Author: Carl Worth <cworth@cworth.org>
-#
-# Based on "notmuch help" as follows:
-#
-# Usage: notmuch <command> [args...]
-#
-# Where <command> and [args...] are as follows:
-#
-#      setup
-#
-#      new
-#
-#      search [options] <search-term> [...]
-#
-#      show <search-terms>
-#
-#      reply <search-terms>
-#
-#      tag +<tag>|-<tag> [...] [--] <search-terms> [...]
-#
-#      dump [<filename>]
-#
-#      restore <filename>
-#
-#      help [<command>]
-
-_notmuch()
-{
-    local current previous commands help_options
-
-    previous=${COMP_WORDS[COMP_CWORD-1]}
-    current="${COMP_WORDS[COMP_CWORD]}"
-
-    commands="setup new search show reply tag dump restore help"
-    help_options="setup new search show reply tag dump restore search-terms"
-    search_options="--max-threads= --first= --sort="
-
-    COMPREPLY=()
-
-    case $COMP_CWORD in
-        1)
-            COMPREPLY=( $(compgen -W "${commands}" -- ${current}) ) ;;
-        2)
-            case $previous in
-                help)
-                    COMPREPLY=( $(compgen -W "${help_options}" -- ${current}) ) ;;
-                search)
-                    COMPREPLY=( $(compgen -W "${search_options}" -- ${current}) ) ;;
-            esac
-            ;;
-    esac
-}
-
-complete -o default -o bashdefault -F _notmuch notmuch
diff --git a/contrib/notmuch-completion.tcsh b/contrib/notmuch-completion.tcsh
deleted file mode 100644 (file)
index c0d3a44..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-set NOTMUCH_CMD=`notmuch help | awk '/\t/' | cut -f2 |grep -v '^$'`
-complete notmuch 'p/1/$NOTMUCH_CMD/'
diff --git a/contrib/notmuch-completion.zsh b/contrib/notmuch-completion.zsh
deleted file mode 100644 (file)
index 67a9aba..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-#compdef notmuch
-
-# ZSH completion for `notmuch`
-# Copyright © 2009 Ingmar Vanhassel <ingmar@exherbo.org>
-
-_notmuch_commands()
-{
-  local -a notmuch_commands
-  notmuch_commands=(
-    'setup:interactively set up notmuch for first use'
-    'new:find and import any new message to the database'
-    'search:search for messages matching the search terms, display matching threads as results'
-    'reply:constructs a reply template for a set of messages'
-    'show:show all messages matching the search terms'
-    'tag:add or remove tags for all messages matching the search terms'
-    'dump:creates a plain-text dump of the tags of each message'
-    'restore:restores the tags from the given file'
-    'help:show details on a command'
-  )
-
-  _describe -t command 'command' notmuch_commands
-}
-
-_notmuch_dump()
-{
-  _files
-}
-
-_notmuch_help_topics()
-{
-  local -a notmuch_help_topics
-  notmuch_help_topics=(
-    'search-terms:show common search-terms syntax'
-  )
-  _describe -t notmuch-help-topics 'topic' notmuch_help_topics
-}
-
-_notmuch_help()
-{
-  _alternative \
-    _notmuch_commands \
-    _notmuch_help_topics
-}
-
-_notmuch_restore()
-{
-  _files
-}
-
-_notmuch_search()
-{
-  _arguments -s : \
-    '--max-threads=[display only the first x threads from the search results]:number of threads to show: ' \
-    '--first=[omit the first x threads from the search results]:number of threads to omit: ' \
-    '--sort=[sort results]:sorting:((newest-first\:"reverse chronological order" oldest-first\:"chronological order"))'
-}
-
-_notmuch()
-{
-  if (( CURRENT > 2 )) ; then
-    local cmd=${words[2]}
-    curcontext="${curcontext%:*:*}:notmuch-$cmd"
-    (( CURRENT-- ))
-    shift words
-    _call_function ret _notmuch_$cmd
-    return ret
-  else
-    _notmuch_commands
-  fi
-}
-
-_notmuch "$@"
-
-# vim: set sw=2 sts=2 ts=2 et ft=zsh :
diff --git a/emacs/Makefile b/emacs/Makefile
new file mode 100644 (file)
index 0000000..b6859ea
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makfefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/emacs/Makefile.local b/emacs/Makefile.local
new file mode 100644 (file)
index 0000000..52aca4e
--- /dev/null
@@ -0,0 +1,29 @@
+# -*- makefile -*-
+
+dir := emacs
+emacs_sources := \
+       $(dir)/notmuch-lib.el \
+       $(dir)/notmuch.el \
+       $(dir)/notmuch-query.el \
+       $(dir)/notmuch-show.el
+
+emacs_bytecode := $(subst .el,.elc,$(emacs_sources))
+
+%.elc: %.el
+       $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $<
+
+ifeq ($(HAVE_EMACS),1)
+all: $(emacs_bytecode)
+endif
+
+install: install-emacs
+
+.PHONY: install-emacs
+install-emacs:
+       mkdir -p $(DESTDIR)/$(emacs_lispdir)
+       install -m0644 $(emacs_sources) $(DESTDIR)$(emacs_lispdir)
+ifeq ($(HAVE_EMACS),1)
+       install -m0644 $(emacs_bytecode) $(DESTDIR)$(emacs_lispdir)
+endif
+
+CLEAN := $(CLEAN) $(emacs_bytecode)
diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
new file mode 100644 (file)
index 0000000..f4454be
--- /dev/null
@@ -0,0 +1,53 @@
+;; notmuch-lib.el --- common variables, functions and function declarations
+;;
+;; Copyright © Carl Worth
+;;
+;; 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: Carl Worth <cworth@cworth.org>
+
+;; This is an part of an emacs-based interface to the notmuch mail system.
+
+(defvar notmuch-command "notmuch"
+  "Command to run the notmuch binary.")
+
+(declare-function notmuch-toggle-invisible-action "notmuch" (cite-button))
+
+(define-button-type 'notmuch-button-invisibility-toggle-type
+  'action 'notmuch-toggle-invisible-action
+  'follow-link t
+  'face 'font-lock-comment-face)
+
+(define-button-type 'notmuch-button-headers-toggle-type
+  'help-echo "mouse-1, RET: Show headers"
+  :supertype 'notmuch-button-invisibility-toggle-type)
+
+;; XXX: This should be a generic function in emacs somewhere, not
+;; here.
+(defun point-invisible-p ()
+  "Return whether the character at point is invisible.
+
+Here visibility is determined by `buffer-invisibility-spec' and
+the invisible property of any overlays for point. It doesn't have
+anything to do with whether point is currently being displayed
+within the current window."
+  (let ((prop (get-char-property (point) 'invisible)))
+    (if (eq buffer-invisibility-spec t)
+       prop
+      (or (memq prop buffer-invisibility-spec)
+         (assq prop buffer-invisibility-spec)))))
+
+(provide 'notmuch-lib)
diff --git a/emacs/notmuch-query.el b/emacs/notmuch-query.el
new file mode 100644 (file)
index 0000000..0d6e775
--- /dev/null
@@ -0,0 +1,81 @@
+;; notmuch-query.el --- provide an emacs api to query notmuch
+;;
+;; Copyright © David Bremner
+;;
+;; 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 Bremner <david@tethera.net>
+
+(require 'notmuch-lib)
+(require 'json)
+
+(defun notmuch-query-get-threads (search-terms &rest options)
+  "Return a list of threads of messages matching SEARCH-TERMS.
+
+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 (append '("show" "--format=json") search-terms))
+        (json-object-type 'plist)
+        (json-array-type 'list)
+        (json-false 'nil))
+    (with-temp-buffer
+      (progn
+       (apply 'call-process (append (list notmuch-command nil t nil) args))
+       (goto-char (point-min))
+       (json-read)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Mapping functions across collections of messages.
+
+(defun notmuch-query-map-aux  (mapper function seq)
+  "private function to do the actual mapping and flattening"
+  (apply 'append
+        (mapcar
+          (lambda (tree)
+            (funcall mapper fn tree))
+          seq)))
+
+(defun notmuch-query-map-threads (fn threads)
+  "apply FN to every thread in  THREADS. Flatten results to a list.
+
+See the function notmuch-query-get-threads for more information."
+  (notmuch-query-map-aux 'notmuch-query-map-forest fn threads))
+
+(defun notmuch-query-map-forest (fn forest)
+  "apply function to every message in a forest. Flatten results to a list.
+
+See the function notmuch-query-get-threads for more information.
+"
+  (notmuch-query-map-aux 'notmuch-query-map-tree fn forest))
+
+(defun notmuch-query-map-tree (fn tree)
+  "Apply function FN to every message in TREE. Flatten results to a list
+
+See the function notmuch-query-get-threads for more information."
+  (cons (funcall fn (car tree)) (notmuch-query-map-forest fn (cadr tree))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predefined queries
+
+(defun notmuch-query-get-message-ids (&rest search-terms)
+  "Return a list of message-ids of messages that match SEARCH-TERMS"
+  (notmuch-query-map-threads
+   (lambda (msg) (plist-get msg :id))
+   (notmuch-query-get-threads search-terms)))
+
+(provide 'notmuch-query)
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
new file mode 100644 (file)
index 0000000..cc1f905
--- /dev/null
@@ -0,0 +1,997 @@
+;; notmuch-show.el --- display notmuch messages within emacs
+;;
+;; Copyright © Carl Worth
+;;
+;; 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: Carl Worth <cworth@cworth.org>
+
+;; This is an part of an emacs-based interface to the notmuch mail system.
+
+(require 'cl)
+(require 'mm-view)
+(require 'message)
+
+(require 'notmuch-lib)
+
+(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
+(declare-function notmuch-count-attachments "notmuch" (mm-handle))
+(declare-function notmuch-reply "notmuch" (query-string))
+(declare-function notmuch-fontify-headers "notmuch" nil)
+(declare-function notmuch-toggle-invisible-action "notmuch" (cite-button))
+(declare-function notmuch-select-tag-with-completion "notmuch" (prompt &rest search-terms))
+(declare-function notmuch-search-show-thread "notmuch" nil)
+(declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp))
+
+(defvar notmuch-show-stash-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "c" 'notmuch-show-stash-cc)
+    (define-key map "d" 'notmuch-show-stash-date)
+    (define-key map "F" 'notmuch-show-stash-filename)
+    (define-key map "f" 'notmuch-show-stash-from)
+    (define-key map "i" 'notmuch-show-stash-message-id)
+    (define-key map "s" 'notmuch-show-stash-subject)
+    (define-key map "T" 'notmuch-show-stash-tags)
+    (define-key map "t" 'notmuch-show-stash-to)
+    map)
+  "Submap for stash commands"
+  )
+
+(fset 'notmuch-show-stash-map notmuch-show-stash-map)
+
+(defvar notmuch-show-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "?" 'notmuch-help)
+    (define-key map "q" 'kill-this-buffer)
+    (define-key map (kbd "C-p") 'notmuch-show-previous-line)
+    (define-key map (kbd "C-n") 'notmuch-show-next-line)
+    (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
+    (define-key map (kbd "TAB") 'notmuch-show-next-button)
+    (define-key map "s" 'notmuch-search)
+    (define-key map "m" 'message-mail)
+    (define-key map "f" 'notmuch-show-forward-current)
+    (define-key map "r" 'notmuch-show-reply)
+    (define-key map "|" 'notmuch-show-pipe-message)
+    (define-key map "w" 'notmuch-show-save-attachments)
+    (define-key map "V" 'notmuch-show-view-raw-message)
+    (define-key map "v" 'notmuch-show-view-all-mime-parts)
+    (define-key map "c" 'notmuch-show-stash-map)
+    (define-key map "b" 'notmuch-show-toggle-current-body)
+    (define-key map "h" 'notmuch-show-toggle-current-header)
+    (define-key map "-" 'notmuch-show-remove-tag)
+    (define-key map "+" 'notmuch-show-add-tag)
+    (define-key map "x" 'notmuch-show-archive-thread-then-exit)
+    (define-key map "a" 'notmuch-show-archive-thread)
+    (define-key map "P" 'notmuch-show-previous-message)
+    (define-key map "N" 'notmuch-show-next-message)
+    (define-key map "p" 'notmuch-show-previous-open-message)
+    (define-key map "n" 'notmuch-show-next-open-message)
+    (define-key map (kbd "DEL") 'notmuch-show-rewind)
+    (define-key map " " 'notmuch-show-advance-and-archive)
+    map)
+  "Keymap for \"notmuch show\" buffers.")
+(fset 'notmuch-show-mode-map notmuch-show-mode-map)
+
+(defvar notmuch-show-signature-regexp "\\(-- ?\\|_+\\)$"
+  "Pattern to match a line that separates content from signature.
+
+The regexp can (and should) include $ to match the end of the
+line, but should not include ^ to match the beginning of the
+line. This is because notmuch may have inserted additional space
+for indentation at the beginning of the line. But notmuch will
+move past the indentation when testing this pattern, (so that the
+pattern can still test against the entire line).")
+
+(defvar notmuch-show-signature-button-format
+  "[ %d-line signature. Click/Enter to toggle visibility. ]"
+  "String used to construct button text for hidden signatures
+
+Can use up to one integer format parameter, i.e. %d")
+
+(defvar notmuch-show-citation-button-format
+  "[ %d more citation lines. Click/Enter to toggle visibility. ]"
+  "String used to construct button text for hidden citations.
+
+Can use up to one integer format parameter, i.e. %d")
+
+(defvar notmuch-show-signature-lines-max 12
+  "Maximum length of signature that will be hidden by default.")
+
+(defvar notmuch-show-citation-lines-prefix 4
+  "Always show at least this many lines of a citation.
+
+If there is one more line, show that, otherwise collapse
+remaining lines into a button.")
+
+(defvar notmuch-show-message-begin-regexp    "\fmessage{")
+(defvar notmuch-show-message-end-regexp      "\fmessage}")
+(defvar notmuch-show-header-begin-regexp     "\fheader{")
+(defvar notmuch-show-header-end-regexp       "\fheader}")
+(defvar notmuch-show-body-begin-regexp       "\fbody{")
+(defvar notmuch-show-body-end-regexp         "\fbody}")
+(defvar notmuch-show-attachment-begin-regexp "\fattachment{")
+(defvar notmuch-show-attachment-end-regexp   "\fattachment}")
+(defvar notmuch-show-part-begin-regexp       "\fpart{")
+(defvar notmuch-show-part-end-regexp         "\fpart}")
+(defvar notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$")
+
+(defvar notmuch-show-id-regexp "\\(id:[^ ]*\\)")
+(defvar notmuch-show-depth-match-regexp " depth:\\([0-9]*\\).*match:\\([01]\\) ")
+(defvar notmuch-show-filename-regexp "filename:\\(.*\\)$")
+(defvar notmuch-show-contentype-regexp "Content-type: \\(.*\\)")
+
+(defvar notmuch-show-tags-regexp "(\\([^)]*\\))$")
+
+(defvar notmuch-show-parent-buffer nil)
+(defvar notmuch-show-body-read-visible nil)
+(defvar notmuch-show-citations-visible nil)
+(defvar notmuch-show-signatures-visible nil)
+(defvar notmuch-show-headers-visible nil)
+
+(defun notmuch-show-next-line ()
+  "Like builtin `next-line' but ensuring we end on a visible character.
+
+By advancing forward until reaching a visible character.
+
+Unlike builtin `next-line' this version accepts no arguments."
+  (interactive)
+  (set 'this-command 'next-line)
+  (call-interactively 'next-line)
+  (while (point-invisible-p)
+    (forward-char)))
+
+(defun notmuch-show-previous-line ()
+  "Like builtin `previous-line' but ensuring we end on a visible character.
+
+By advancing forward until reaching a visible character.
+
+Unlike builtin `previous-line' this version accepts no arguments."
+  (interactive)
+  (set 'this-command 'previous-line)
+  (call-interactively 'previous-line)
+  (while (point-invisible-p)
+    (forward-char)))
+
+(defun notmuch-show-get-message-id ()
+  (save-excursion
+    (beginning-of-line)
+    (if (not (looking-at notmuch-show-message-begin-regexp))
+       (re-search-backward notmuch-show-message-begin-regexp))
+    (re-search-forward notmuch-show-id-regexp)
+    (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
+
+(defun notmuch-show-get-filename ()
+  (save-excursion
+    (beginning-of-line)
+    (if (not (looking-at notmuch-show-message-begin-regexp))
+       (re-search-backward notmuch-show-message-begin-regexp))
+    (re-search-forward notmuch-show-filename-regexp)
+    (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
+
+(defun notmuch-show-set-tags (tags)
+  (save-excursion
+    (beginning-of-line)
+    (if (not (looking-at notmuch-show-message-begin-regexp))
+       (re-search-backward notmuch-show-message-begin-regexp))
+    (re-search-forward notmuch-show-tags-regexp)
+    (let ((inhibit-read-only t)
+         (beg (match-beginning 1))
+         (end (match-end 1)))
+      (delete-region beg end)
+      (goto-char beg)
+      (insert (mapconcat 'identity tags " ")))))
+
+(defun notmuch-show-get-tags ()
+  (save-excursion
+    (beginning-of-line)
+    (if (not (looking-at notmuch-show-message-begin-regexp))
+       (re-search-backward notmuch-show-message-begin-regexp))
+    (re-search-forward notmuch-show-tags-regexp)
+    (split-string (buffer-substring (match-beginning 1) (match-end 1)))))
+
+(defun notmuch-show-get-bcc ()
+  "Return BCC address(es) of current message"
+  (notmuch-show-get-header-field 'bcc))
+
+(defun notmuch-show-get-cc ()
+  "Return CC address(es) of current message"
+  (notmuch-show-get-header-field 'cc))
+
+(defun notmuch-show-get-date ()
+  "Return Date of current message"
+  (notmuch-show-get-header-field 'date))
+
+(defun notmuch-show-get-from ()
+  "Return From address of current message"
+  (notmuch-show-get-header-field 'from))
+
+(defun notmuch-show-get-subject ()
+  "Return Subject of current message"
+  (notmuch-show-get-header-field 'subject))
+
+(defun notmuch-show-get-to ()
+  "Return To address(es) of current message"
+  (notmuch-show-get-header-field 'to))
+
+(defun notmuch-show-get-header-field (name)
+  "Retrieve the header field NAME from the current message.
+NAME should be a symbol, in lower case, as returned by
+mail-header-extract-no-properties"
+  (let* ((result (assoc name (notmuch-show-get-header)))
+        (val (and result (cdr result))))
+    val))
+
+(defun notmuch-show-get-header ()
+  "Retrieve and parse the header from the current message. Returns an alist with of (header . value)
+where header is a symbol and value is a string.  The summary from notmuch-show is returned as the
+pseudoheader summary"
+  (require 'mailheader)
+  (save-excursion
+    (beginning-of-line)
+    (if (not (looking-at notmuch-show-message-begin-regexp))
+       (re-search-backward notmuch-show-message-begin-regexp))
+    (re-search-forward (concat notmuch-show-header-begin-regexp "\n[[:space:]]*\\(.*\\)\n"))
+    (let* ((summary (buffer-substring-no-properties (match-beginning 1) (match-end 1)))
+         (beg (point)))
+      (re-search-forward notmuch-show-header-end-regexp)
+      (let ((text (buffer-substring beg (match-beginning 0))))
+       (with-temp-buffer
+         (insert text)
+         (goto-char (point-min))
+         (while (looking-at "\\([[:space:]]*\\)[A-Za-z][-A-Za-z0-9]*:")
+           (delete-region (match-beginning 1) (match-end 1))
+           (forward-line)
+           )
+         (goto-char (point-min))
+         (cons (cons 'summary summary) (mail-header-extract-no-properties)))))))
+
+(defun notmuch-show-add-tag (&rest toadd)
+  "Add a tag to the current message."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Tag to add: ")))
+  (apply 'notmuch-call-notmuch-process
+        (append (cons "tag"
+                      (mapcar (lambda (s) (concat "+" s)) toadd))
+                (cons (notmuch-show-get-message-id) nil)))
+  (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
+
+(defun notmuch-show-remove-tag (&rest toremove)
+  "Remove a tag from the current message."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-show-get-message-id))))
+  (let ((tags (notmuch-show-get-tags)))
+    (if (intersection tags toremove :test 'string=)
+       (progn
+         (apply 'notmuch-call-notmuch-process
+                (append (cons "tag"
+                              (mapcar (lambda (s) (concat "-" s)) toremove))
+                        (cons (notmuch-show-get-message-id) nil)))
+         (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
+
+(defun notmuch-show-archive-thread ()
+  "Archive each message in thread, then show next thread from search.
+
+Archive each message currently shown by removing the \"inbox\"
+tag from each. Then kill this buffer and show the next thread
+from the search from which this thread was originally shown.
+
+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)
+  (save-excursion
+    (goto-char (point-min))
+    (while (not (eobp))
+      (notmuch-show-remove-tag "inbox")
+      (if (not (eobp))
+         (forward-char))
+      (if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
+         (goto-char (point-max)))))
+  (let ((parent-buffer notmuch-show-parent-buffer))
+    (kill-this-buffer)
+    (if parent-buffer
+       (progn
+         (switch-to-buffer parent-buffer)
+         (forward-line)
+         (notmuch-search-show-thread)))))
+
+(defun notmuch-show-archive-thread-then-exit ()
+  "Archive each message in thread, then exit back to search results."
+  (interactive)
+  (notmuch-show-archive-thread)
+  (kill-this-buffer))
+
+(defun notmuch-show-view-raw-message ()
+  "View the raw email of the current message."
+  (interactive)
+  (view-file (notmuch-show-get-filename)))
+
+(defmacro with-current-notmuch-show-message (&rest body)
+  "Evaluate body with current buffer set to the text of current message"
+  `(save-excursion
+     (let ((filename (notmuch-show-get-filename)))
+       (let ((buf (generate-new-buffer (concat "*notmuch-msg-" filename "*"))))
+         (with-current-buffer buf
+           (insert-file-contents filename nil nil nil t)
+           ,@body)
+        (kill-buffer buf)))))
+
+(defun notmuch-show-view-all-mime-parts ()
+  "Use external viewers to view all attachments from the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   ; We ovverride the mm-inline-media-tests to indicate which message
+   ; parts are already sufficiently handled by the original
+   ; presentation of the message in notmuch-show mode. These parts
+   ; will be inserted directly into the temporary buffer of
+   ; with-current-notmuch-show-message and silently discarded.
+   ;
+   ; Any MIME part not explicitly mentioned here will be handled by an
+   ; external viewer as configured in the various mailcap files.
+   (let ((mm-inline-media-tests '(
+                                 ("text/.*" ignore identity)
+                                 ("application/pgp-signature" ignore identity)
+                                 ("multipart/alternative" ignore identity)
+                                 ("multipart/mixed" ignore identity)
+                                 ("multipart/related" ignore identity)
+                                )))
+     (mm-display-parts (mm-dissect-buffer)))))
+
+(defun notmuch-show-save-attachments ()
+  "Save all attachments from the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   (let ((mm-handle (mm-dissect-buffer)))
+     (notmuch-save-attachments
+      mm-handle (> (notmuch-count-attachments mm-handle) 1))))
+  (message "Done"))
+
+(defun notmuch-show-reply ()
+  "Begin composing a reply to the current message in a new buffer."
+  (interactive)
+  (let ((message-id (notmuch-show-get-message-id)))
+    (notmuch-reply message-id)))
+
+(defun notmuch-show-forward-current ()
+  "Forward the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   (message-forward)))
+
+(defun notmuch-show-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 *Messages* buffer."
+  (interactive "sPipe message to command: ")
+  (apply 'start-process-shell-command "notmuch-pipe-command" "*notmuch-pipe*"
+        (list command " < " (shell-quote-argument (notmuch-show-get-filename)))))
+
+(defun notmuch-show-move-to-current-message-summary-line ()
+  "Move to the beginning of the one-line summary of the current message.
+
+This gives us a stable place to move to and work from since the
+summary line is always visible. This is important since moving to
+an invisible location is unreliable, (the main command loop moves
+point either forward or backward to the next visible character
+when a command ends with point on an invisible character).
+
+Emits an error if point is not within a valid message, (that is
+no pattern of `notmuch-show-message-begin-regexp' could be found
+by searching backward)."
+  (beginning-of-line)
+  (if (not (looking-at notmuch-show-message-begin-regexp))
+      (if (re-search-backward notmuch-show-message-begin-regexp nil t)
+         (forward-line 2)
+       (error "Not within a valid message."))
+    (forward-line 2)))
+
+(defun notmuch-show-last-message-p ()
+  "Predicate testing whether point is within the last message."
+  (save-window-excursion
+    (save-excursion
+      (notmuch-show-move-to-current-message-summary-line)
+      (not (re-search-forward notmuch-show-message-begin-regexp nil t)))))
+
+(defun notmuch-show-message-unread-p ()
+  "Predicate testing whether current message is unread."
+  (member "unread" (notmuch-show-get-tags)))
+
+(defun notmuch-show-message-open-p ()
+  "Predicate testing whether current message is open (body is visible)."
+  (let ((btn (previous-button (point) t)))
+    (while (not (button-has-type-p btn 'notmuch-button-body-toggle-type))
+      (setq btn (previous-button (button-start btn))))
+    (not (invisible-p (button-get btn 'invisibility-spec)))))
+
+(defun notmuch-show-next-message-without-marking-read ()
+  "Advance to the beginning of the next message in the buffer.
+
+Moves to the last visible character of the current message if
+already on the last message in the buffer.
+
+Returns nil if already on the last message in the buffer."
+  (notmuch-show-move-to-current-message-summary-line)
+  (if (re-search-forward notmuch-show-message-begin-regexp nil t)
+      (progn
+       (notmuch-show-move-to-current-message-summary-line)
+       (recenter 0)
+       t)
+    (goto-char (- (point-max) 1))
+    (while (point-invisible-p)
+      (backward-char))
+    (recenter 0)
+    nil))
+
+(defun notmuch-show-next-message ()
+  "Advance to the next message (whether open or closed)
+and remove the unread tag from that message.
+
+Moves to the last visible character of the current message if
+already on the last message in the buffer.
+
+Returns nil if already on the last message in the buffer."
+  (interactive)
+  (notmuch-show-next-message-without-marking-read)
+  (notmuch-show-mark-read))
+
+(defun notmuch-show-find-next-message ()
+  "Returns the position of the next message in the buffer.
+
+Or the position of the last visible character of the current
+message if already within the last message in the buffer."
+  ; save-excursion doesn't save our window position
+  ; save-window-excursion doesn't save point
+  ; Looks like we have to use both.
+  (save-excursion
+    (save-window-excursion
+      (notmuch-show-next-message-without-marking-read)
+      (point))))
+
+(defun notmuch-show-next-unread-message ()
+  "Advance to the next unread message.
+
+Moves to the last visible character of the current message if
+there are no more unread messages past the current point."
+  (notmuch-show-next-message-without-marking-read)
+  (while (and (not (notmuch-show-last-message-p))
+             (not (notmuch-show-message-unread-p)))
+    (notmuch-show-next-message-without-marking-read))
+  (if (not (notmuch-show-message-unread-p))
+      (notmuch-show-next-message-without-marking-read))
+  (notmuch-show-mark-read))
+
+(defun notmuch-show-next-open-message ()
+  "Advance to the next open message (that is, body is visible).
+
+Moves to the last visible character of the final message in the buffer
+if there are no more open messages."
+  (interactive)
+  (while (and (notmuch-show-next-message-without-marking-read)
+             (not (notmuch-show-message-open-p))))
+  (notmuch-show-mark-read))
+
+(defun notmuch-show-previous-message-without-marking-read ()
+  "Backup to the beginning of the previous message in the buffer.
+
+If within a message rather than at the beginning of it, then
+simply move to the beginning of the current message.
+
+Returns nil if already on the first message in the buffer."
+  (let ((start (point)))
+    (notmuch-show-move-to-current-message-summary-line)
+    (if (not (< (point) start))
+       ; Go backward twice to skip the current message's marker
+       (progn
+         (re-search-backward notmuch-show-message-begin-regexp nil t)
+         (re-search-backward notmuch-show-message-begin-regexp nil t)
+         (notmuch-show-move-to-current-message-summary-line)
+         (recenter 0)
+         (if (= (point) start)
+             nil
+           t))
+      (recenter 0)
+      nil)))
+
+(defun notmuch-show-previous-message ()
+  "Backup to the previous message (whether open or closed)
+and remove the unread tag from that message.
+
+If within a message rather than at the beginning of it, then
+simply move to the beginning of the current message."
+  (interactive)
+  (notmuch-show-previous-message-without-marking-read)
+  (notmuch-show-mark-read))
+
+(defun notmuch-show-find-previous-message ()
+  "Returns the position of the previous message in the buffer.
+
+Or the position of the beginning of the current message if point
+is originally within the message rather than at the beginning of
+it."
+  ; save-excursion doesn't save our window position
+  ; save-window-excursion doesn't save point
+  ; Looks like we have to use both.
+  (save-excursion
+    (save-window-excursion
+      (notmuch-show-previous-message-without-marking-read)
+      (point))))
+
+(defun notmuch-show-previous-open-message ()
+  "Backup to previous open message (that is, body is visible).
+
+Moves to the first message in the buffer if there are no previous
+open messages."
+  (interactive)
+  (while (and (notmuch-show-previous-message-without-marking-read)
+             (not (notmuch-show-message-open-p))))
+  (notmuch-show-mark-read))
+
+(defun notmuch-show-rewind ()
+  "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
+
+Specifically, if the beginning of the previous email is fewer
+than `window-height' lines from the current point, move to it
+just like `notmuch-show-previous-message'.
+
+Otherwise, just scroll down a screenful of the current message.
+
+This command does not modify any message tags, (it does not undo
+any effects from previous calls to
+`notmuch-show-advance-and-archive'."
+  (interactive)
+  (let ((previous (notmuch-show-find-previous-message)))
+    (if (> (count-lines previous (point)) (- (window-height) next-screen-context-lines))
+       (progn
+         (condition-case nil
+             (scroll-down nil)
+           ((beginning-of-buffer) nil))
+         (goto-char (window-start))
+         ; Because count-lines counts invivisible lines, we may have
+         ; scrolled to far. If so., notice this and fix it up.
+         (if (< (point) previous)
+             (progn
+               (goto-char previous)
+               (recenter 0))))
+      (notmuch-show-previous-message))))
+
+(defun notmuch-show-advance-and-archive ()
+  "Advance through thread and archive.
+
+This command is intended to be one of the simplest ways to
+process a thread of email. It does the following:
+
+If the current message in the thread is not yet fully visible,
+scroll by a near screenful to read more of the message.
+
+Otherwise, (the end of the current message is already within the
+current window), advance to the next open message.
+
+Finally, if there is no further message to advance to, and this
+last message is already read, then archive the entire current
+thread, (remove the \"inbox\" tag from each message). Also kill
+this buffer, and display the next thread from the search from
+which this thread was originally shown."
+  (interactive)
+  (let ((next (notmuch-show-find-next-message))
+       (unread (notmuch-show-message-unread-p)))
+    (if (> next (window-end))
+       (scroll-up nil)
+      (let ((last (notmuch-show-last-message-p)))
+       (notmuch-show-next-open-message)
+       (if last
+           (notmuch-show-archive-thread))))))
+
+(defun notmuch-show-next-button ()
+  "Advance point to the next button in the buffer."
+  (interactive)
+  (forward-button 1))
+
+(defun notmuch-show-previous-button ()
+  "Move point back to the previous button in the buffer."
+  (interactive)
+  (backward-button 1))
+
+(defun notmuch-show-toggle-current-body ()
+  "Toggle the display of the current message body."
+  (interactive)
+  (save-excursion
+    (notmuch-show-move-to-current-message-summary-line)
+    (unless (button-at (point))
+      (notmuch-show-next-button))
+    (push-button))
+  )
+
+(defun notmuch-show-toggle-current-header ()
+  "Toggle the display of the current message header."
+  (interactive)
+  (save-excursion
+    (notmuch-show-move-to-current-message-summary-line)
+    (forward-line)
+    (unless (button-at (point))
+      (notmuch-show-next-button))
+    (push-button))
+  )
+
+(defun notmuch-show-citation-regexp (depth)
+  "Build a regexp for matching citations at a given DEPTH (indent)"
+  (let ((line-regexp (format "[[:space:]]\\{%d\\}>.*\n" depth)))
+    (concat "\\(?:^" line-regexp
+           "\\(?:[[:space:]]*\n" line-regexp
+           "\\)?\\)+")))
+
+(defun notmuch-show-region-to-button (beg end type prefix button-text)
+  "Auxilary function to do the actual making of overlays and buttons
+
+BEG and END are buffer locations. TYPE should a string, either
+\"citation\" or \"signature\". PREFIX is some arbitrary text to
+insert before the button, probably for indentation.  BUTTON-TEXT
+is what to put on the button."
+
+;; This uses some slightly tricky conversions between strings and
+;; symbols because of the way the button code works. Note that
+;; replacing intern-soft with make-symbol will cause this to fail,
+;; since the newly created symbol has no plist.
+
+  (let ((overlay (make-overlay beg end))
+       (invis-spec (make-symbol (concat "notmuch-" type "-region")))
+       (button-type (intern-soft (concat "notmuch-button-"
+                                         type "-toggle-type"))))
+    (add-to-invisibility-spec invis-spec)
+    (overlay-put overlay 'invisible invis-spec)
+    (goto-char (1+ end))
+    (save-excursion
+      (goto-char (1- beg))
+      (insert prefix)
+      (insert-button button-text
+                    'invisibility-spec invis-spec
+                    :type button-type)
+      )))
+
+(defun notmuch-show-markup-citations-region (beg end depth)
+  "Markup citations, and up to one signature in the given region"
+  ;; it would be nice if the untabify was not required, but
+  ;; that would require notmuch to indent with spaces.
+  (untabify beg end)
+  (let ((citation-regexp (notmuch-show-citation-regexp depth))
+       (signature-regexp (concat (format "^[[:space:]]\\{%d\\}" depth)
+                                 notmuch-show-signature-regexp))
+       (indent (concat "\n" (make-string depth ? ))))
+    (goto-char beg)
+    (beginning-of-line)
+    (while (and (< (point) end)
+               (re-search-forward citation-regexp end t))
+      (let* ((cite-start (match-beginning 0))
+            (cite-end  (match-end 0))
+            (cite-lines (count-lines cite-start cite-end)))
+       (when (> cite-lines (1+ notmuch-show-citation-lines-prefix))
+         (goto-char cite-start)
+         (forward-line notmuch-show-citation-lines-prefix)
+         (notmuch-show-region-to-button
+          (point) cite-end
+          "citation"
+          indent
+          (format notmuch-show-citation-button-format
+                  (- cite-lines notmuch-show-citation-lines-prefix))
+          ))))
+    (if (and (< (point) end)
+            (re-search-forward signature-regexp end t))
+       (let* ((sig-start (match-beginning 0))
+              (sig-end (match-end 0))
+              (sig-lines (1- (count-lines sig-start end))))
+         (if (<= sig-lines notmuch-show-signature-lines-max)
+             (notmuch-show-region-to-button
+              sig-start
+              end
+              "signature"
+              indent
+              (format notmuch-show-signature-button-format sig-lines)
+              ))))))
+
+(defun notmuch-show-markup-part (beg end depth)
+  (if (re-search-forward notmuch-show-part-begin-regexp nil t)
+      (progn
+        (let (mime-message mime-type)
+          (save-excursion
+            (re-search-forward notmuch-show-contentype-regexp end t)
+            (setq mime-type (car (split-string (buffer-substring
+                                                (match-beginning 1) (match-end 1))))))
+
+          (if (equal mime-type "text/html")
+              (let ((filename (notmuch-show-get-filename)))
+                (with-temp-buffer
+                  (insert-file-contents filename nil nil nil t)
+                  (setq mime-message (mm-dissect-buffer)))))
+          (forward-line)
+          (let ((beg (point-marker)))
+            (re-search-forward notmuch-show-part-end-regexp)
+            (let ((end (copy-marker (match-beginning 0))))
+              (goto-char end)
+              (if (not (bolp))
+                  (insert "\n"))
+              (indent-rigidly beg end depth)
+              (if (not (eq mime-message nil))
+                  (save-excursion
+                    (goto-char beg)
+                    (forward-line -1)
+                    (let ((handle-type (mm-handle-type mime-message))
+                          mime-type)
+                      (if (sequencep (car handle-type))
+                          (setq mime-type (car handle-type))
+                        (setq mime-type (car (car (cdr handle-type))))
+                        )
+                      (if (equal mime-type "text/html")
+                          (mm-display-part mime-message))))
+                )
+              (notmuch-show-markup-citations-region beg end depth)
+              ; Advance to the next part (if any) (so the outer loop can
+              ; determine whether we've left the current message.
+              (if (re-search-forward notmuch-show-part-begin-regexp nil t)
+                  (beginning-of-line)))))
+        (goto-char end))
+    (goto-char end)))
+
+(defun notmuch-show-markup-parts-region (beg end depth)
+  (save-excursion
+    (goto-char beg)
+    (while (< (point) end)
+      (notmuch-show-markup-part beg end depth))))
+
+(defun notmuch-show-markup-body (depth match btn)
+  "Markup a message body, (indenting, buttonizing citations,
+etc.), and hiding the body itself if the message does not match
+the current search.
+
+DEPTH specifies the depth at which this message appears in the
+tree of the current thread, (the top-level messages have depth 0
+and each reply increases depth by 1). MATCH indicates whether
+this message is regarded as matching the current search. BTN is
+the button which is used to toggle the visibility of this
+message.
+
+When this function is called, point must be within the message, but
+before the delimiter marking the beginning of the body."
+  (re-search-forward notmuch-show-body-begin-regexp)
+  (forward-line)
+  (let ((beg (point-marker)))
+    (re-search-forward notmuch-show-body-end-regexp)
+    (let ((end (copy-marker (match-beginning 0))))
+      (notmuch-show-markup-parts-region beg end depth)
+      (let ((invis-spec (make-symbol "notmuch-show-body-read")))
+        (overlay-put (make-overlay beg end)
+                     'invisible invis-spec)
+        (button-put btn 'invisibility-spec invis-spec)
+        (if (not match)
+            (add-to-invisibility-spec invis-spec)))
+      (set-marker beg nil)
+      (set-marker end nil)
+      )))
+
+(defun notmuch-show-markup-header (message-begin depth)
+  "Buttonize and decorate faces in a message header.
+
+MESSAGE-BEGIN is the position of the absolute first character in
+the message (including all delimiters that will end up being
+invisible etc.). This is to allow a button to reliably extend to
+the beginning of the message even if point is positioned at an
+invisible character (such as the beginning of the buffer).
+
+DEPTH specifies the depth at which this message appears in the
+tree of the current thread, (the top-level messages have depth 0
+and each reply increases depth by 1)."
+  (re-search-forward notmuch-show-header-begin-regexp)
+  (forward-line)
+  (let ((beg (point-marker))
+       (summary-end (copy-marker (line-beginning-position 2)))
+       (subject-end (copy-marker (line-end-position 2)))
+       (invis-spec (make-symbol "notmuch-show-header"))
+        (btn nil))
+    (re-search-forward notmuch-show-header-end-regexp)
+    (beginning-of-line)
+    (let ((end (point-marker)))
+      (indent-rigidly beg end depth)
+      (goto-char beg)
+      (setq btn (make-button message-begin summary-end :type 'notmuch-button-body-toggle-type))
+      (forward-line)
+      (add-to-invisibility-spec invis-spec)
+      (overlay-put (make-overlay subject-end end)
+                  'invisible invis-spec)
+      (make-button (line-beginning-position) subject-end
+                  'invisibility-spec invis-spec
+                  :type 'notmuch-button-headers-toggle-type)
+      (while (looking-at "[[:space:]]*[A-Za-z][-A-Za-z0-9]*:")
+       (beginning-of-line)
+       (notmuch-fontify-headers)
+       (forward-line)
+       )
+      (goto-char end)
+      (insert "\n")
+      (set-marker beg nil)
+      (set-marker summary-end nil)
+      (set-marker subject-end nil)
+      (set-marker end nil)
+      )
+  btn))
+
+(defun notmuch-show-markup-message ()
+  (if (re-search-forward notmuch-show-message-begin-regexp nil t)
+      (let ((message-begin (match-beginning 0)))
+       (re-search-forward notmuch-show-depth-match-regexp)
+       (let ((depth (string-to-number (buffer-substring (match-beginning 1) (match-end 1))))
+             (match (string= "1" (buffer-substring (match-beginning 2) (match-end 2))))
+              (btn nil))
+         (setq btn (notmuch-show-markup-header message-begin depth))
+         (notmuch-show-markup-body depth match btn)))
+    (goto-char (point-max))))
+
+(defun notmuch-show-hide-markers ()
+  (save-excursion
+    (goto-char (point-min))
+    (while (not (eobp))
+      (if (re-search-forward notmuch-show-marker-regexp nil t)
+         (progn
+           (overlay-put (make-overlay (match-beginning 0) (+ (match-end 0) 1))
+                        'invisible 'notmuch-show-marker))
+       (goto-char (point-max))))))
+
+(defun notmuch-show-markup-messages ()
+  (save-excursion
+    (goto-char (point-min))
+    (while (not (eobp))
+      (notmuch-show-markup-message)))
+  (notmuch-show-hide-markers))
+
+;;;###autoload
+(defun notmuch-show-mode ()
+  "Major mode for viewing a thread with notmuch.
+
+This buffer contains the results of the \"notmuch show\" command
+for displaying a single thread of email from your email archives.
+
+By default, various components of email messages, (citations,
+signatures, already-read messages), are hidden. You can make
+these parts visible by clicking with the mouse button or by
+pressing RET after positioning the cursor on a hidden part, (for
+which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
+
+Reading the thread sequentially is well-supported by pressing
+\\[notmuch-show-advance-and-archive]. This will
+scroll the current message (if necessary), advance to the next
+message, or advance to the next thread (if already on the last
+message of a thread).
+
+Other commands are available to read or manipulate the thread more
+selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages without
+removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread without
+scrolling through with \\[notmuch-show-advance-and-archive]).
+
+You can add or remove arbitary tags from the current message with
+'\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
+
+All currently available key bindings:
+
+\\{notmuch-show-mode-map}"
+  (interactive)
+  (kill-all-local-variables)
+  (add-to-invisibility-spec 'notmuch-show-marker)
+  (use-local-map notmuch-show-mode-map)
+  (setq major-mode 'notmuch-show-mode
+       mode-name "notmuch-show")
+  (setq buffer-read-only t))
+
+(defcustom notmuch-show-hook nil
+  "List of functions to call when notmuch displays a message."
+  :type 'hook
+  :options '(goto-address)
+  :group 'notmuch)
+
+(defun notmuch-show-do-stash (text)
+    (kill-new text)
+    (message (concat "Saved: " text)))
+
+(defun notmuch-show-stash-cc ()
+  "Copy CC field of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-cc)))
+
+(defun notmuch-show-stash-date ()
+  "Copy date of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-date)))
+
+(defun notmuch-show-stash-filename ()
+  "Copy filename of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-filename)))
+
+(defun notmuch-show-stash-from ()
+  "Copy From address of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-from)))
+
+(defun notmuch-show-stash-message-id ()
+  "Copy message ID of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-message-id)))
+
+(defun notmuch-show-stash-subject ()
+  "Copy Subject field of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-subject)))
+
+(defun notmuch-show-stash-tags ()
+  "Copy tags of current message to kill-ring as a comma separated list."
+  (interactive)
+  (notmuch-show-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
+
+(defun notmuch-show-stash-to ()
+  "Copy To address of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-to)))
+
+; Make show mode a bit prettier, highlighting URLs and using word wrap
+
+(defun notmuch-show-mark-read ()
+  (notmuch-show-remove-tag "unread"))
+
+(defun notmuch-show-pretty-hook ()
+  (goto-address-mode 1)
+  (visual-line-mode))
+
+(add-hook 'notmuch-show-hook 'notmuch-show-mark-read)
+(add-hook 'notmuch-show-hook 'notmuch-show-pretty-hook)
+(add-hook 'notmuch-search-hook
+         (lambda()
+           (hl-line-mode 1) ))
+
+(defun notmuch-show (thread-id &optional parent-buffer query-context)
+  "Run \"notmuch show\" with the given thread ID and display results.
+
+The optional PARENT-BUFFER is the notmuch-search buffer from
+which this notmuch-show command was executed, (so that the next
+thread from that buffer can be show when done with this one).
+
+The optional QUERY-CONTEXT is a notmuch search term. Only messages from the thread
+matching this search term are shown if non-nil. "
+  (interactive "sNotmuch show: ")
+  (let ((buffer (get-buffer-create (concat "*notmuch-show-" thread-id "*"))))
+    (switch-to-buffer buffer)
+    (notmuch-show-mode)
+    (set (make-local-variable 'notmuch-show-parent-buffer) parent-buffer)
+    (let ((proc (get-buffer-process (current-buffer)))
+         (inhibit-read-only t))
+      (if proc
+         (error "notmuch search process already running for query `%s'" thread-id)
+       )
+      (erase-buffer)
+      (goto-char (point-min))
+      (save-excursion
+       (let* ((basic-args (list notmuch-command nil t nil "show" "--entire-thread" thread-id))
+               (args (if query-context (append basic-args (list "and (" query-context ")")) basic-args)))
+         (apply 'call-process args)
+         (when (and (eq (buffer-size) 0) query-context)
+           (apply 'call-process basic-args)))
+       (notmuch-show-markup-messages)
+       )
+      (run-hooks 'notmuch-show-hook)
+      ; Move straight to the first open message
+      (if (not (notmuch-show-message-open-p))
+         (notmuch-show-next-open-message))
+      )))
+
+(provide 'notmuch-show)
diff --git a/emacs/notmuch.el b/emacs/notmuch.el
new file mode 100644 (file)
index 0000000..5e479d6
--- /dev/null
@@ -0,0 +1,812 @@
+; notmuch.el --- run notmuch within emacs
+;
+; Copyright © Carl Worth
+;
+; 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: Carl Worth <cworth@cworth.org>
+
+; This is an emacs-based interface to the notmuch mail system.
+;
+; You will first need to have the notmuch program installed and have a
+; notmuch database built in order to use this. See
+; http://notmuchmail.org for details.
+;
+; To install this software, copy it to a directory that is on the
+; `load-path' variable within emacs (a good candidate is
+; /usr/local/share/emacs/site-lisp). If you are viewing this from the
+; notmuch source distribution then you can simply run:
+;
+;      sudo make install-emacs
+;
+; to install it.
+;
+; Then, to actually run it, add:
+;
+;      (require 'notmuch)
+;
+; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
+; or run:
+;
+;      emacs -f notmuch
+;
+; Have fun, and let us know if you have any comment, questions, or
+; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
+; required, but is available from http://notmuchmail.org).
+
+(require 'cl)
+(require 'mm-view)
+(require 'message)
+
+(require 'notmuch-lib)
+(require 'notmuch-show)
+
+(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+  (let ((tag-list
+        (with-output-to-string
+          (with-current-buffer standard-output
+            (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
+    (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
+
+(defun notmuch-foreach-mime-part (function mm-handle)
+  (cond ((stringp (car mm-handle))
+         (dolist (part (cdr mm-handle))
+           (notmuch-foreach-mime-part function part)))
+        ((bufferp (car mm-handle))
+         (funcall function mm-handle))
+        (t (dolist (part mm-handle)
+             (notmuch-foreach-mime-part function part)))))
+
+(defun notmuch-count-attachments (mm-handle)
+  (let ((count 0))
+    (notmuch-foreach-mime-part
+     (lambda (p)
+       (let ((disposition (mm-handle-disposition p)))
+         (and (listp disposition)
+              (or (equal (car disposition) "attachment")
+                  (and (equal (car disposition) "inline")
+                       (assq 'filename disposition)))
+              (incf count))))
+     mm-handle)
+    count))
+
+(defun notmuch-save-attachments (mm-handle &optional queryp)
+  (notmuch-foreach-mime-part
+   (lambda (p)
+     (let ((disposition (mm-handle-disposition p)))
+       (and (listp disposition)
+            (or (equal (car disposition) "attachment")
+                (and (equal (car disposition) "inline")
+                     (assq 'filename disposition)))
+            (or (not queryp)
+                (y-or-n-p
+                 (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
+            (mm-save-part p))))
+   mm-handle))
+
+(defun notmuch-reply (query-string)
+  (switch-to-buffer (generate-new-buffer "notmuch-draft"))
+  (call-process notmuch-command nil t nil "reply" query-string)
+  (message-insert-signature)
+  (goto-char (point-min))
+  (if (re-search-forward "^$" nil t)
+      (progn
+       (insert "--text follows this line--")
+       (forward-line)))
+  (message-mode))
+
+(defun notmuch-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)
+          ))
+  (force-window-update)
+  (redisplay t))
+
+(define-button-type 'notmuch-button-citation-toggle-type 'help-echo "mouse-1, RET: Show citation"
+  :supertype 'notmuch-button-invisibility-toggle-type)
+(define-button-type 'notmuch-button-signature-toggle-type 'help-echo "mouse-1, RET: Show signature"
+  :supertype 'notmuch-button-invisibility-toggle-type)
+(define-button-type 'notmuch-button-body-toggle-type
+  'help-echo "mouse-1, RET: Show message"
+  'face 'notmuch-message-summary-face
+  :supertype 'notmuch-button-invisibility-toggle-type)
+
+(defun notmuch-fontify-headers ()
+  (while (looking-at "[[:space:]]")
+    (forward-char))
+  (if (looking-at "[Tt]o:")
+      (progn
+       (overlay-put (make-overlay (point) (re-search-forward ":"))
+                    'face 'message-header-name)
+       (overlay-put (make-overlay (point) (re-search-forward ".*$"))
+                    'face 'message-header-to))
+    (if (looking-at "[B]?[Cc][Cc]:")
+       (progn
+         (overlay-put (make-overlay (point) (re-search-forward ":"))
+                      'face 'message-header-name)
+         (overlay-put (make-overlay (point) (re-search-forward ".*$"))
+                      'face 'message-header-cc))
+      (if (looking-at "[Ss]ubject:")
+         (progn
+           (overlay-put (make-overlay (point) (re-search-forward ":"))
+                        'face 'message-header-name)
+           (overlay-put (make-overlay (point) (re-search-forward ".*$"))
+                        'face 'message-header-subject))
+       (if (looking-at "[Ff]rom:")
+           (progn
+             (overlay-put (make-overlay (point) (re-search-forward ":"))
+                          'face 'message-header-name)
+             (overlay-put (make-overlay (point) (re-search-forward ".*$"))
+                          'face 'message-header-other)))))))
+
+(defun notmuch-documentation-first-line (symbol)
+  "Return the first line of the documentation string for SYMBOL."
+  (let ((doc (documentation symbol)))
+    (if doc
+       (with-temp-buffer
+         (insert (documentation symbol t))
+         (goto-char (point-min))
+         (let ((beg (point)))
+           (end-of-line)
+           (buffer-substring beg (point))))
+      "")))
+
+(defun notmuch-prefix-key-description (key)
+  "Given a prefix key code, return a human-readable string representation.
+
+This is basically just `format-kbd-macro' but we also convert ESC to M-."
+  (let ((desc (format-kbd-macro (vector key))))
+    (if (string= desc "ESC")
+       "M-"
+      (concat desc " "))))
+
+; I would think that emacs would have code handy for walking a keymap
+; and generating strings for each key, and I would prefer to just call
+; that. But I couldn't find any (could be all implemented in C I
+; suppose), so I wrote my own here.
+(defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
+  "For a key binding, return a string showing a human-readable
+representation of the prefixed key as well as the first line of
+documentation from the bound function.
+
+For a mouse binding, return nil."
+  (let ((key (car binding))
+       (action (cdr binding)))
+    (if (mouse-event-p key)
+       nil
+      (if (keymapp action)
+         (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
+               (as-list))
+           (map-keymap (lambda (a b)
+                         (push (cons a b) as-list))
+                       action)
+           (mapconcat substitute as-list "\n"))
+       (concat prefix (format-kbd-macro (vector key))
+               "\t"
+               (notmuch-documentation-first-line action))))))
+
+(defalias 'notmuch-substitute-one-command-key
+  (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil))
+
+(defun notmuch-substitute-command-keys (doc)
+  "Like `substitute-command-keys' but with documentation, not function names."
+  (let ((beg 0))
+    (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
+      (let ((map (substring doc (match-beginning 1) (match-end 1))))
+       (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key
+                                           (cdr (symbol-value (intern map))) "\n") 1 1 doc)))
+      (setq beg (match-end 0)))
+    doc))
+
+(defun notmuch-help ()
+  "Display help for the current notmuch mode."
+  (interactive)
+  (let* ((mode major-mode)
+        (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
+    (with-current-buffer (generate-new-buffer "*notmuch-help*")
+      (insert doc)
+      (goto-char (point-min))
+      (set-buffer-modified-p nil)
+      (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
+
+(defgroup notmuch nil
+  "Notmuch mail reader for Emacs."
+  :group 'mail)
+
+(defcustom notmuch-search-hook nil
+  "List of functions to call when notmuch displays the search results."
+  :type 'hook
+  :options '(hl-line-mode)
+  :group 'notmuch)
+
+(defvar notmuch-search-authors-width 20
+  "Number of columns to use to display authors in a notmuch-search buffer.")
+
+(defvar notmuch-search-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "?" 'notmuch-help)
+    (define-key map "q" 'kill-this-buffer)
+    (define-key map "x" 'kill-this-buffer)
+    (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
+    (define-key map "b" 'notmuch-search-scroll-down)
+    (define-key map " " 'notmuch-search-scroll-up)
+    (define-key map "<" 'notmuch-search-first-thread)
+    (define-key map ">" 'notmuch-search-last-thread)
+    (define-key map "p" 'notmuch-search-previous-thread)
+    (define-key map "n" 'notmuch-search-next-thread)
+    (define-key map "r" 'notmuch-search-reply-to-thread)
+    (define-key map "m" 'message-mail)
+    (define-key map "s" 'notmuch-search)
+    (define-key map "o" 'notmuch-search-toggle-order)
+    (define-key map "=" 'notmuch-search-refresh-view)
+    (define-key map "t" 'notmuch-search-filter-by-tag)
+    (define-key map "f" 'notmuch-search-filter)
+    (define-key map [mouse-1] 'notmuch-search-show-thread)
+    (define-key map "*" 'notmuch-search-operate-all)
+    (define-key map "a" 'notmuch-search-archive-thread)
+    (define-key map "-" 'notmuch-search-remove-tag)
+    (define-key map "+" 'notmuch-search-add-tag)
+    (define-key map (kbd "RET") 'notmuch-search-show-thread)
+    map)
+  "Keymap for \"notmuch search\" buffers.")
+(fset 'notmuch-search-mode-map notmuch-search-mode-map)
+
+(defvar notmuch-search-query-string)
+(defvar notmuch-search-target-thread)
+(defvar notmuch-search-target-line)
+(defvar notmuch-search-oldest-first t
+  "Show the oldest mail first in the search-mode")
+
+(defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
+
+(defun notmuch-search-scroll-up ()
+  "Move forward through search results by one window's worth."
+  (interactive)
+  (condition-case nil
+      (scroll-up nil)
+    ((end-of-buffer) (notmuch-search-last-thread))))
+
+(defun notmuch-search-scroll-down ()
+  "Move backward through the search results by one window's worth."
+  (interactive)
+  ; I don't know why scroll-down doesn't signal beginning-of-buffer
+  ; the way that scroll-up signals end-of-buffer, but c'est la vie.
+  ;
+  ; So instead of trapping a signal we instead check whether the
+  ; window begins on the first line of the buffer and if so, move
+  ; directly to that position. (We have to count lines since the
+  ; window-start position is not the same as point-min due to the
+  ; invisible thread-ID characters on the first line.
+  (if (equal (count-lines (point-min) (window-start)) 0)
+      (goto-char (point-min))
+    (scroll-down nil)))
+
+(defun notmuch-search-next-thread ()
+  "Select the next thread in the search results."
+  (interactive)
+  (forward-line 1))
+
+(defun notmuch-search-previous-thread ()
+  "Select the previous thread in the search results."
+  (interactive)
+  (forward-line -1))
+
+(defun notmuch-search-last-thread ()
+  "Select the last thread in the search results."
+  (interactive)
+  (goto-char (point-max))
+  (forward-line -2))
+
+(defun notmuch-search-first-thread ()
+  "Select the first thread in the search results."
+  (interactive)
+  (goto-char (point-min)))
+
+(defface notmuch-message-summary-face
+ '((((class color) (background light)) (:background "#f0f0f0"))
+   (((class color) (background dark)) (:background "#303030")))
+ "Face for the single-line message summary in notmuch-show-mode."
+ :group 'notmuch)
+
+(defface notmuch-tag-face
+  '((((class color)
+      (background dark))
+     (:foreground "OliveDrab1"))
+    (((class color)
+      (background light))
+     (:foreground "navy blue" :bold t))
+    (t
+     (:bold t)))
+  "Notmuch search mode face used to highligh tags."
+  :group 'notmuch)
+
+(defvar notmuch-tag-face-alist nil
+  "List containing the tag list that need to be highlighed")
+
+(defvar notmuch-search-font-lock-keywords  nil)
+
+;;;###autoload
+(defun notmuch-search-mode ()
+  "Major mode displaying results of a notmuch search.
+
+This buffer contains the results of a \"notmuch search\" of your
+email archives. Each line in the buffer represents a single
+thread giving a summary of the thread (a relative date, the
+number of matched messages and total messages in the thread,
+participants in the thread, a representative subject line, and
+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-operate-all]' key can be used to add or remove a tag from all
+threads in the current 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
+only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
+search.
+
+Complete list of currently available key bindings:
+
+\\{notmuch-search-mode-map}"
+  (interactive)
+  (kill-all-local-variables)
+  (make-local-variable 'notmuch-search-query-string)
+  (make-local-variable 'notmuch-search-oldest-first)
+  (make-local-variable 'notmuch-search-target-thread)
+  (make-local-variable 'notmuch-search-target-line)
+  (set (make-local-variable 'scroll-preserve-screen-position) t)
+  (add-to-invisibility-spec 'notmuch-search)
+  (use-local-map notmuch-search-mode-map)
+  (setq truncate-lines t)
+  (setq major-mode 'notmuch-search-mode
+       mode-name "notmuch-search")
+  (setq buffer-read-only t)
+  (if (not notmuch-tag-face-alist)
+      (add-to-list 'notmuch-search-font-lock-keywords (list
+               "(\\([^()]*\\))$" '(1  'notmuch-tag-face)))
+    (let ((notmuch-search-tags (mapcar 'car notmuch-tag-face-alist)))
+      (loop for notmuch-search-tag  in notmuch-search-tags
+           do (add-to-list 'notmuch-search-font-lock-keywords (list
+                       (concat "([^)]*\\(" notmuch-search-tag "\\)[^)]*)$")
+                       `(1  ,(cdr (assoc notmuch-search-tag notmuch-tag-face-alist))))))))
+  (set (make-local-variable 'font-lock-defaults)
+         '(notmuch-search-font-lock-keywords t)))
+
+(defun notmuch-search-find-thread-id ()
+  "Return the thread for the current thread"
+  (get-text-property (point) 'notmuch-search-thread-id))
+
+(defun notmuch-search-find-authors ()
+  "Return the authors for the current thread"
+  (get-text-property (point) 'notmuch-search-authors))
+
+(defun notmuch-search-find-subject ()
+  "Return the subject for the current thread"
+  (get-text-property (point) 'notmuch-search-subject))
+
+(defun notmuch-search-show-thread ()
+  "Display the currently selected thread."
+  (interactive)
+  (let ((thread-id (notmuch-search-find-thread-id)))
+    (if (> (length thread-id) 0)
+       (notmuch-show thread-id (current-buffer) notmuch-search-query-string)
+      (error "End of search results"))))
+
+(defun notmuch-search-reply-to-thread ()
+  "Begin composing a reply to the entire current thread in a new buffer."
+  (interactive)
+  (let ((message-id (notmuch-search-find-thread-id)))
+    (notmuch-reply message-id)))
+
+(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 (mapconcat  'identity tags " "))))))
+
+(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 beg end))))))
+
+(defun notmuch-search-add-tag (tag)
+  "Add a tag to the currently selected thread.
+
+The tag is added to messages in the currently selected thread
+which match the current search terms."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Tag to add: ")))
+  (notmuch-call-notmuch-process "tag" (concat "+" tag) (notmuch-search-find-thread-id))
+  (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))))
+
+(defun notmuch-search-remove-tag (tag)
+  "Remove a tag from the currently selected thread.
+
+The tag is removed from all messages in the currently selected thread."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-search-find-thread-id))))
+  (notmuch-call-notmuch-process "tag" (concat "-" tag) (notmuch-search-find-thread-id))
+  (notmuch-search-set-tags (delete tag (notmuch-search-get-tags))))
+
+(defun notmuch-search-archive-thread ()
+  "Archive the currently selected thread (remove its \"inbox\" tag).
+
+This function advances the next thread when finished."
+  (interactive)
+  (notmuch-search-remove-tag "inbox")
+  (forward-line))
+
+(defun notmuch-search-process-sentinel (proc msg)
+  "Add a message to let user know when \"notmuch search\" exits"
+  (let ((buffer (process-buffer proc))
+       (status (process-status proc))
+       (exit-status (process-exit-status proc))
+       (never-found-target-thread nil))
+    (if (memq status '(exit signal))
+       (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 (search process was killed).\n"))
+                 (if (eq status 'exit)
+                     (progn
+                       (insert "End of search results.")
+                       (if (not (= exit-status 0))
+                           (insert (format " (process returned %d)" exit-status)))
+                       (insert "\n")
+                       (if (and atbob
+                                (not (string= notmuch-search-target-thread "found")))
+                           (set 'never-found-target-thread t))))))
+             (if (and never-found-target-thread
+                      notmuch-search-target-line)
+                 (goto-line notmuch-search-target-line)))))))
+
+(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))
+             (while more
+               (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))
+                          (authors-length (length authors))
+                          (subject (match-string 5 string))
+                          (tags (match-string 6 string)))
+                     (if (> authors-length notmuch-search-authors-width)
+                         (set 'authors (concat (substring authors 0 (- notmuch-search-authors-width 3)) "...")))
+                     (goto-char (point-max))
+                     (let ((beg (point-marker))
+                           (format-string (format "%%s %%-7s %%-%ds %%s (%%s)\n" notmuch-search-authors-width)))
+                       (insert (format format-string date count authors subject tags))
+                       (put-text-property beg (point-marker) 'notmuch-search-thread-id thread-id)
+                       (put-text-property beg (point-marker) 'notmuch-search-authors authors)
+                       (put-text-property beg (point-marker) 'notmuch-search-subject subject)
+                       (if (string= thread-id notmuch-search-target-thread)
+                           (progn
+                             (set 'found-target beg)
+                             (set 'notmuch-search-target-thread "found"))))
+                     (set 'line (match-end 0)))
+                 (set 'more nil)))))
+         (if found-target
+             (goto-char found-target)))
+      (delete-process proc))))
+
+(defun notmuch-search-operate-all (action)
+  "Add/remove tags from all matching messages.
+
+Tis command adds or removes tags from all messages matching the
+current search terms. When called interactively, this command
+will prompt for tags to be added or removed. Tags prefixed with
+'+' will be added and tags prefixed with '-' will be removed.
+
+Each character of the tag name may consist of alphanumeric
+characters as well as `_.+-'.
+"
+  (interactive "sOperation (+add -drop): notmuch tag ")
+  (let ((action-split (split-string action " +")))
+    ;; Perform some validation
+    (let ((words action-split))
+      (when (null words) (error "No operation given"))
+      (while words
+       (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
+         (error "Action must be of the form `+thistag -that_tag'"))
+       (setq words (cdr words))))
+    (apply 'notmuch-call-notmuch-process "tag"
+          (append action-split (list notmuch-search-query-string) nil))))
+
+;;;###autoload
+(defun notmuch-search (query &optional oldest-first target-thread target-line)
+  "Run \"notmuch search\" with the given query string and display results.
+
+The 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
+                 current if it appears in the search results.
+  target-line: The line number to move to if the target thread does not
+               appear in the search results."
+  (interactive "sNotmuch search: ")
+  (let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
+    (switch-to-buffer buffer)
+    (notmuch-search-mode)
+    (set 'notmuch-search-query-string query)
+    (set 'notmuch-search-oldest-first oldest-first)
+    (set 'notmuch-search-target-thread target-thread)
+    (set 'notmuch-search-target-line target-line)
+    (let ((proc (get-buffer-process (current-buffer)))
+         (inhibit-read-only t))
+      (if proc
+         (error "notmuch search process already running for query `%s'" query)
+       )
+      (erase-buffer)
+      (goto-char (point-min))
+      (save-excursion
+       (let ((proc (start-process-shell-command
+                    "notmuch-search" buffer notmuch-command "search"
+                    (if oldest-first "--sort=oldest-first" "--sort=newest-first")
+                    (shell-quote-argument query))))
+         (set-process-sentinel proc 'notmuch-search-process-sentinel)
+         (set-process-filter proc 'notmuch-search-process-filter))))
+    (run-hooks 'notmuch-search-hook)))
+
+(defun notmuch-search-refresh-view ()
+  "Refresh the current view.
+
+Kills the current buffer and runs a new search with the same
+query string as the current search. If the current thread is in
+the new search results, then point will be placed on the same
+thread. Otherwise, point will be moved to attempt to be in the
+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))
+       (query notmuch-search-query-string))
+    (kill-this-buffer)
+    (notmuch-search query oldest-first target-thread target-line)
+    (goto-char (point-min))
+    ))
+
+(defun notmuch-search-toggle-order ()
+  "Toggle the current search order.
+
+By default, the \"inbox\" view created by `notmuch' is displayed
+in chronological order (oldest thread at the beginning of the
+buffer), while any global searches created by `notmuch-search'
+are displayed in reverse-chronological order (newest thread at
+the beginning of the buffer).
+
+This command toggles the sort order for the current search.
+
+Note that any filtered searches created by
+`notmuch-search-filter' retain the search order of the parent
+search."
+  (interactive)
+  (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
+  (notmuch-search-refresh-view))
+
+(defun notmuch-search-filter (query)
+  "Filter the current search results based on an additional query string.
+
+Runs a new search matching only messages that match both the
+current search results AND the additional query string provided."
+  (interactive "sFilter search: ")
+  (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query) (concat "( " query " )") query)))
+    (notmuch-search (concat notmuch-search-query-string " and " grouped-query) notmuch-search-oldest-first)))
+
+(defun notmuch-search-filter-by-tag (tag)
+  "Filter the current search results based on a single tag.
+
+Runs a new search matching only messages that match both the
+current search results AND that are tagged with the given tag."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Filter by tag: ")))
+  (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
+
+;;;###autoload
+(defun notmuch ()
+  "Run notmuch to display all mail with tag of 'inbox'"
+  (interactive)
+  (notmuch-search "tag:inbox" notmuch-search-oldest-first))
+
+(setq mail-user-agent 'message-user-agent)
+
+(defvar notmuch-folder-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "?" 'notmuch-help)
+    (define-key map "x" 'kill-this-buffer)
+    (define-key map "q" 'kill-this-buffer)
+    (define-key map "m" 'message-mail)
+    (define-key map "e" 'notmuch-folder-show-empty-toggle)
+    (define-key map ">" 'notmuch-folder-last)
+    (define-key map "<" 'notmuch-folder-first)
+    (define-key map "=" 'notmuch-folder)
+    (define-key map "s" 'notmuch-search)
+    (define-key map [mouse-1] 'notmuch-folder-show-search)
+    (define-key map (kbd "RET") 'notmuch-folder-show-search)
+    (define-key map " " 'notmuch-folder-show-search)
+    (define-key map "p" 'notmuch-folder-previous)
+    (define-key map "n" 'notmuch-folder-next)
+    map)
+  "Keymap for \"notmuch folder\" buffers.")
+
+(fset 'notmuch-folder-mode-map notmuch-folder-mode-map)
+
+(defcustom notmuch-folders (quote (("inbox" . "tag:inbox") ("unread" . "tag:unread")))
+  "List of searches for the notmuch folder view"
+  :type '(alist :key-type (string) :value-type (string))
+  :group 'notmuch)
+
+(defun notmuch-folder-mode ()
+  "Major mode for showing notmuch 'folders'.
+
+This buffer contains a list of message counts returned by a
+customizable set of searches of your email archives. Each line in
+the buffer shows the name of a saved search and the resulting
+message count.
+
+Pressing RET on any line opens a search window containing the
+results for the saved search on that line.
+
+Here is an example of how the search list could be
+customized, (the following text would be placed in your ~/.emacs
+file):
+
+(setq notmuch-folders '((\"inbox\" . \"tag:inbox\")
+                        (\"unread\" . \"tag:inbox AND tag:unread\")
+                        (\"notmuch\" . \"tag:inbox AND to:notmuchmail.org\")))
+
+Of course, you can have any number of folders, each configured
+with any supported search terms (see \"notmuch help search-terms\").
+
+Currently available key bindings:
+
+\\{notmuch-folder-mode-map}"
+  (interactive)
+  (kill-all-local-variables)
+  (use-local-map 'notmuch-folder-mode-map)
+  (setq truncate-lines t)
+  (hl-line-mode 1)
+  (setq major-mode 'notmuch-folder-mode
+       mode-name "notmuch-folder")
+  (setq buffer-read-only t))
+
+(defun notmuch-folder-next ()
+  "Select the next folder in the list."
+  (interactive)
+  (forward-line 1)
+  (if (eobp)
+      (forward-line -1)))
+
+(defun notmuch-folder-previous ()
+  "Select the previous folder in the list."
+  (interactive)
+  (forward-line -1))
+
+(defun notmuch-folder-first ()
+  "Select the first folder in the list."
+  (interactive)
+  (goto-char (point-min)))
+
+(defun notmuch-folder-last ()
+  "Select the last folder in the list."
+  (interactive)
+  (goto-char (point-max))
+  (forward-line -1))
+
+(defun notmuch-folder-count (search)
+  (car (process-lines notmuch-command "count" search)))
+
+(defvar notmuch-folder-show-empty t
+  "Whether `notmuch-folder-mode' should display empty folders.")
+
+(defun notmuch-folder-show-empty-toggle ()
+  "Toggle the listing of empty folders"
+  (interactive)
+  (setq notmuch-folder-show-empty (not notmuch-folder-show-empty))
+  (notmuch-folder))
+
+(defun notmuch-folder-add (folders)
+  (if folders
+      (let* ((name (car (car folders)))
+           (inhibit-read-only t)
+           (search (cdr (car folders)))
+           (count (notmuch-folder-count search)))
+       (if (or notmuch-folder-show-empty
+               (not (equal count "0")))
+           (progn
+             (insert name)
+             (indent-to 16 1)
+             (insert count)
+             (insert "\n")
+             )
+         )
+       (notmuch-folder-add (cdr folders)))))
+
+(defun notmuch-folder-find-name ()
+  (save-excursion
+    (beginning-of-line)
+    (let ((beg (point)))
+      (re-search-forward "\\([ \t]*[^ \t]+\\)")
+      (filter-buffer-substring (match-beginning 1) (match-end 1)))))
+
+(defun notmuch-folder-show-search (&optional folder)
+  "Show a search window for the search related to the specified folder."
+  (interactive)
+  (if (null folder)
+      (setq folder (notmuch-folder-find-name)))
+  (let ((search (assoc folder notmuch-folders)))
+    (if search
+       (notmuch-search (cdr search) notmuch-search-oldest-first))))
+
+;;;###autoload
+(defun notmuch-folder ()
+  "Show the notmuch folder view and update the displayed counts."
+  (interactive)
+  (let ((buffer (get-buffer-create "*notmuch-folders*")))
+    (switch-to-buffer buffer)
+    (let ((inhibit-read-only t)
+         (n (line-number-at-pos)))
+      (erase-buffer)
+      (notmuch-folder-mode)
+      (notmuch-folder-add notmuch-folders)
+      (goto-char (point-min))
+      (goto-line n))))
+
+(provide 'notmuch)
diff --git a/json.c b/json.c
new file mode 100644 (file)
index 0000000..f90b0fa
--- /dev/null
+++ b/json.c
@@ -0,0 +1,109 @@
+/* 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;
+
+    if (len == 0)
+       return (char *)"\"\"";
+
+    for (loop = 0, required = 0, ptr = str;
+        loop < len;
+        loop++, required++, ptr++) {
+       if (*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 (*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)
+{
+    return (json_quote_chararray (ctx, str, strlen (str)));
+}
index 70489e17207b13c8dad862ff63221277cf65a191..03a458b250d05c84f6cef18e66320a3722148466 100644 (file)
@@ -1,5 +1,33 @@
-dir=lib
-extra_cflags += -I$(dir)
+# -*- makefile -*-
+
+# The major version of the library interface. This will control the soname.
+# As such, this number must be incremented for any incompatible change to
+# the library interface, (such as the deletion of an API or a major
+# semantic change that breaks formerly functioning code).
+#
+# Note: We don't currently have plans to increment this at this time.
+# If we *do* want to make an incompatible change to the library
+# interface, we'll have to decide whether to increment this (creating
+# a new soname) or to introduce symbol versioning to be able to
+# provide support for both old and new interfaces without having to
+# increment this.
+LIBNOTMUCH_VERSION_MAJOR = 1
+
+# The minor version of the library interface. This should be incremented at
+# the time of release for any additions to the library interface.
+LIBNOTMUCH_VERSION_MINOR = 0
+
+# The release version the library interface. This should be incremented at
+# the time of release if there have been no changes to the interface, (but
+# simply compatible changes to the implementation).
+LIBNOTMUCH_VERSION_RELEASE = 0
+
+LINKER_NAME = libnotmuch.so
+SONAME = $(LINKER_NAME).$(LIBNOTMUCH_VERSION_MAJOR)
+LIBNAME = $(SONAME).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE)
+
+dir := lib
+extra_cflags += -I$(dir) -fPIC
 
 libnotmuch_c_srcs =            \
        $(dir)/libsha1.c        \
@@ -18,8 +46,28 @@ libnotmuch_cxx_srcs =                \
        $(dir)/thread.cc
 
 libnotmuch_modules = $(libnotmuch_c_srcs:.c=.o) $(libnotmuch_cxx_srcs:.cc=.o)
-$(dir)/notmuch.a: $(libnotmuch_modules)
+
+$(dir)/libnotmuch.a: $(libnotmuch_modules)
        $(call quiet,AR) rcs $@ $^
 
+$(dir)/$(LIBNAME): $(libnotmuch_modules)
+       $(call quiet,CXX $(CXXFLAGS)) $^ $(FINAL_LDFLAGS) -shared -Wl,-soname=$(SONAME) -o $@
+
+$(dir)/$(SONAME): $(dir)/$(LIBNAME)
+       ln -sf $(LIBNAME) $@
+
+$(dir)/$(LINKER_NAME): $(dir)/$(SONAME)
+       ln -sf $(LIBNAME) $@
+
+install: install-$(dir)
+
+install-$(dir):
+       mkdir -p $(DESTDIR)$(libdir)/
+       install -m0644 $(dir)/$(LIBNAME) $(DESTDIR)$(libdir)/
+       ln -sf $(LIBNAME) $(DESTDIR)$(libdir)/$(SONAME)
+       ln -sf $(LIBNAME) $(DESTDIR)$(libdir)/$(LINKER_NAME)
+       mkdir -p $(DESTDIR)$(prefix)/include/
+       install -m0644 $(dir)/notmuch.h $(DESTDIR)$(prefix)/include/
+
 SRCS  := $(SRCS) $(libnotmuch_c_srcs) $(libnotmuch_cxx_srcs)
-CLEAN := $(CLEAN) $(libnotmuch_modules) $(dir)/notmuch.a
+CLEAN := $(CLEAN) $(libnotmuch_modules) $(dir)/$(SONAME) $(dir)/$(LINKER_NAME) $(dir)$(LIBNAME) libnotmuch.a
index 5891584ec978e5f2868e5c87533462bff083b775..41918d760fe82bed3c960f07f08f30159d3e4231 100644 (file)
 #ifndef NOTMUCH_DATABASE_PRIVATE_H
 #define NOTMUCH_DATABASE_PRIVATE_H
 
+/* According to WG14/N1124, a C++ implementation won't provide us a
+ * macro like PRIx64 (which gives a printf format string for
+ * formatting a uint64_t as hexadecimal) unless we define
+ * __STDC_FORMAT_MACROS before including inttypes.h. That's annoying,
+ * but there it is.
+ */
+#define __STDC_FORMAT_MACROS
+#include <inttypes.h>
+
 #include "notmuch-private.h"
 
 #include <xapian.h>
 
 struct _notmuch_database {
     notmuch_bool_t exception_reported;
+
     char *path;
+
+    notmuch_bool_t needs_upgrade;
     notmuch_database_mode_t mode;
     Xapian::Database *xapian_db;
+
+    uint64_t last_thread_id;
+
     Xapian::QueryParser *query_parser;
     Xapian::TermGenerator *term_gen;
     Xapian::ValueRangeProcessor *value_range_processor;
 
-    notmuch_bool_t needs_upgrade;
 };
 
 /* Convert tags from Xapian internal format to notmuch format.
index cce7847860e5a963532936eb0f9f4fb84af2a5c4..c91e97c125940b384a3bd24963df6cbeedd88acb 100644 (file)
@@ -147,6 +147,7 @@ prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
 prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
     { "thread",                        "G" },
     { "tag",                   "K" },
+    { "is",                    "K" },
     { "id",                    "Q" }
 };
 
@@ -533,6 +534,8 @@ notmuch_database_open (const char *path,
     notmuch->needs_upgrade = FALSE;
     notmuch->mode = mode;
     try {
+       string last_thread_id;
+
        if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
            notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
                                                               Xapian::DB_CREATE_OR_OPEN);
@@ -567,6 +570,20 @@ notmuch_database_open (const char *path,
                         notmuch_path, version, NOTMUCH_DATABASE_VERSION);
            }
        }
+
+       last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
+       if (last_thread_id.empty ()) {
+           notmuch->last_thread_id = 0;
+       } else {
+           const char *str;
+           char *end;
+
+           str = last_thread_id.c_str ();
+           notmuch->last_thread_id = strtoull (str, &end, 16);
+           if (*end != '\0')
+               INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
+       }
+
        notmuch->query_parser = new Xapian::QueryParser;
        notmuch->term_gen = new Xapian::TermGenerator;
        notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
@@ -735,8 +752,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
        total = notmuch_query_count_messages (query);
 
        for (messages = notmuch_query_search_messages (query);
-            notmuch_messages_has_more (messages);
-            notmuch_messages_advance (messages))
+            notmuch_messages_valid (messages);
+            notmuch_messages_move_to_next (messages))
        {
            if (do_progress_notify) {
                progress_notify (closure, (double) count / total);
@@ -811,8 +828,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
        char *filename;
 
        for (messages = notmuch_query_search_messages (query);
-            notmuch_messages_has_more (messages);
-            notmuch_messages_advance (messages))
+            notmuch_messages_valid (messages);
+            notmuch_messages_move_to_next (messages))
        {
            if (do_progress_notify) {
                progress_notify (closure, (double) count / total);
@@ -1278,14 +1295,38 @@ _notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
     return ret;
 }
 
+static const char *
+_notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
+{
+    /* 16 bytes (+ terminator) for hexadecimal representation of
+     * a 64-bit integer. */
+    static char thread_id[17];
+    Xapian::WritableDatabase *db;
+
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+    notmuch->last_thread_id++;
+
+    sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
+
+    db->set_metadata ("last_thread_id", thread_id);
+
+    return thread_id;
+}
+
 /* Given a (mostly empty) 'message' and its corresponding
  * 'message_file' link it to existing threads in the database.
  *
  * We first look at 'message_file' and its link-relevant headers
  * (References and In-Reply-To) for message IDs. We also look in the
- * database for existing message that reference 'message'.
+ * database for existing message that reference 'message'. In either
+ * case, we will assign to the current message the first thread_id
+ * found (through either parent or child). We will also merge any
+ * existing, distinct threads where this message belongs to both,
+ * (which is not uncommon when mesages are processed out of order).
  *
- * The end result is to call _notmuch_message_ensure_thread_id which
+ * Finally, if not thread ID has been found through parent or child,
+ * we call _notmuch_message_generate_thread_id to generate a new
  * generates a new thread ID if the message doesn't connect to any
  * existing threads.
  */
@@ -1308,8 +1349,12 @@ _notmuch_database_link_message (notmuch_database_t *notmuch,
     if (status)
        return status;
 
-    if (thread_id == NULL)
-       _notmuch_message_ensure_thread_id (message);
+    /* If not part of any existing thread, generate a new thread ID. */
+    if (thread_id == NULL) {
+       thread_id = _notmuch_database_generate_thread_id (notmuch);
+
+       _notmuch_message_add_term (message, "thread", thread_id);
+    }
 
     return NOTMUCH_STATUS_SUCCESS;
 }
index bb6314ad7610b992726c8396251df1b64fa32606..5e75b73eb5cfea02d2c7796ae0cd7de7dccf125e 100644 (file)
@@ -79,7 +79,7 @@ _notmuch_filenames_create (void *ctx,
 }
 
 notmuch_bool_t
-notmuch_filenames_has_more (notmuch_filenames_t *filenames)
+notmuch_filenames_valid (notmuch_filenames_t *filenames)
 {
     if (filenames == NULL)
        return NULL;
@@ -105,7 +105,7 @@ notmuch_filenames_get (notmuch_filenames_t *filenames)
 }
 
 void
-notmuch_filenames_advance (notmuch_filenames_t *filenames)
+notmuch_filenames_move_to_next (notmuch_filenames_t *filenames)
 {
     if (filenames == NULL)
        return;
index 7e2da0854aa130b234b8f5e032c2402e840d5602..cf930251c8c7be4c34bb53dab6930cca940d0302 100644 (file)
 #include "notmuch-private.h"
 
 #include <gmime/gmime.h>
+#include <gmime/gmime-filter.h>
 
 #include <xapian.h>
 
+/* Oh, how I wish that gobject didn't require so much noisy boilerplate!
+ * (Though I have at least eliminated some of the stock set...) */
+typedef struct _NotmuchFilterDiscardUuencode NotmuchFilterDiscardUuencode;
+typedef struct _NotmuchFilterDiscardUuencodeClass NotmuchFilterDiscardUuencodeClass;
+
+/**
+ * NotmuchFilterDiscardUuencode:
+ *
+ * @parent_object: parent #GMimeFilter
+ * @encode: encoding vs decoding
+ * @state: State of the parser
+ *
+ * A filter to discard uuencoded portions of an email.
+ *
+ * A uuencoded portion is identified as beginning with a line
+ * matching:
+ *
+ *     begin [0-7][0-7][0-7] .*
+ *
+ * After that detection, and beginning with the following line,
+ * characters will be discarded as long as the first character of each
+ * line begins with M and subsequent characters on the line are within
+ * the range of ASCII characters from ' ' to '`'.
+ *
+ * This is not a perfect UUencode filter. It's possible to have a
+ * message that will legitimately match that pattern, (so that some
+ * legitimate content is discarded). And for most UUencoded files, the
+ * final line of encoded data (the line not starting with M) will be
+ * indexed.
+ **/
+struct _NotmuchFilterDiscardUuencode {
+    GMimeFilter parent_object;
+    int state;
+};
+
+struct _NotmuchFilterDiscardUuencodeClass {
+    GMimeFilterClass parent_class;
+};
+
+GMimeFilter *notmuch_filter_discard_uuencode_new (void);
+
+static void notmuch_filter_discard_uuencode_finalize (GObject *object);
+
+static GMimeFilter *filter_copy (GMimeFilter *filter);
+static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                          char **out, size_t *outlen, size_t *outprespace);
+static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                            char **out, size_t *outlen, size_t *outprespace);
+static void filter_reset (GMimeFilter *filter);
+
+
+static GMimeFilterClass *parent_class = NULL;
+
+static void
+notmuch_filter_discard_uuencode_class_init (NotmuchFilterDiscardUuencodeClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+    GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
+
+    parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
+
+    object_class->finalize = notmuch_filter_discard_uuencode_finalize;
+
+    filter_class->copy = filter_copy;
+    filter_class->filter = filter_filter;
+    filter_class->complete = filter_complete;
+    filter_class->reset = filter_reset;
+}
+
+static void
+notmuch_filter_discard_uuencode_finalize (GObject *object)
+{
+    G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static GMimeFilter *
+filter_copy (GMimeFilter *gmime_filter)
+{
+    (void) gmime_filter;
+    return notmuch_filter_discard_uuencode_new ();
+}
+
+static void
+filter_filter (GMimeFilter *gmime_filter, char *inbuf, size_t inlen, size_t prespace,
+              char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    NotmuchFilterDiscardUuencode *filter = (NotmuchFilterDiscardUuencode *) gmime_filter;
+    register const char *inptr = inbuf;
+    const char *inend = inbuf + inlen;
+    char *outptr;
+
+    (void) prespace;
+
+    /* Simple, linear state-transition diagram for our filter.
+     *
+     * If the character being processed is within the range of [a, b]
+     * for the current state then we transition next_if_match
+     * state. If not, we transition to the next_if_not_match state.
+     *
+     * The final two states are special in that they are the states in
+     * which we discard data. */
+    static const struct {
+       int state;
+       int a;
+       int b;
+       int next_if_match;
+       int next_if_not_match;
+    } states[] = {
+       {0,  'b',  'b',  1,  0},
+       {1,  'e',  'e',  2,  0},
+       {2,  'g',  'g',  3,  0},
+       {3,  'i',  'i',  4,  0},
+       {4,  'n',  'n',  5,  0},
+       {5,  ' ',  ' ',  6,  0},
+       {6,  '0',  '7',  7,  0},
+       {7,  '0',  '7',  8,  0},
+       {8,  '0',  '7',  9,  0},
+       {9,  ' ',  ' ',  10, 0},
+       {10, '\n', '\n', 11, 10},
+       {11, 'M',  'M',  12, 0},
+       {12, ' ',  '`',  12, 11}  
+    };
+    int next;
+
+    g_mime_filter_set_size (gmime_filter, inlen, FALSE);
+    outptr = gmime_filter->outbuf;
+
+    while (inptr < inend) {
+       if (*inptr >= states[filter->state].a &&
+           *inptr <= states[filter->state].b)
+       {
+           next = states[filter->state].next_if_match;
+       }
+       else
+       {
+           next = states[filter->state].next_if_not_match;
+       }
+
+       if (filter->state < 11)
+           *outptr++ = *inptr;
+
+       filter->state = next;
+       inptr++;
+    }
+
+    *outlen = outptr - gmime_filter->outbuf;
+    *outprespace = gmime_filter->outpre;
+    *outbuf = gmime_filter->outbuf;
+}
+
+static void
+filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+                char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    if (inbuf && inlen)
+       filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
+}
+
+static void
+filter_reset (GMimeFilter *gmime_filter)
+{
+    NotmuchFilterDiscardUuencode *filter = (NotmuchFilterDiscardUuencode *) gmime_filter;
+
+    filter->state = 0;
+}
+
+/**
+ * notmuch_filter_discard_uuencode_new:
+ *
+ * Returns: a new #NotmuchFilterDiscardUuencode filter.
+ **/
+GMimeFilter *
+notmuch_filter_discard_uuencode_new (void)
+{
+    static GType type = 0;
+    NotmuchFilterDiscardUuencode *filter;
+
+    if (!type) {
+       static const GTypeInfo info = {
+           sizeof (NotmuchFilterDiscardUuencodeClass),
+           NULL, /* base_class_init */
+           NULL, /* base_class_finalize */
+           (GClassInitFunc) notmuch_filter_discard_uuencode_class_init,
+           NULL, /* class_finalize */
+           NULL, /* class_data */
+           sizeof (NotmuchFilterDiscardUuencode),
+           0,    /* n_preallocs */
+           NULL, /* instance_init */
+           NULL  /* value_table */
+       };
+
+       type = g_type_register_static (GMIME_TYPE_FILTER, "NotmuchFilterDiscardUuencode", &info, (GTypeFlags) 0);
+    }
+
+    filter = (NotmuchFilterDiscardUuencode *) g_object_newv (type, 0, NULL);
+    filter->state = 0;
+
+    return (GMimeFilter *) filter;
+}
+
 /* We're finally down to a single (NAME + address) email "mailbox". */
 static void
 _index_address_mailbox (notmuch_message_t *message,
@@ -128,7 +329,8 @@ static void
 _index_mime_part (notmuch_message_t *message,
                  GMimeObject *part)
 {
-    GMimeStream *stream;
+    GMimeStream *stream, *filter;
+    GMimeFilter *discard_uuencode_filter;
     GMimeDataWrapper *wrapper;
     GByteArray *byte_array;
     GMimeContentDisposition *disposition;
@@ -186,11 +388,20 @@ _index_mime_part (notmuch_message_t *message,
 
     stream = g_mime_stream_mem_new_with_byte_array (byte_array);
     g_mime_stream_mem_set_owner (GMIME_STREAM_MEM (stream), FALSE);
+
+    filter = g_mime_stream_filter_new (stream);
+    discard_uuencode_filter = notmuch_filter_discard_uuencode_new ();
+
+    g_mime_stream_filter_add (GMIME_STREAM_FILTER (filter),
+                             discard_uuencode_filter);
+
     wrapper = g_mime_part_get_content_object (GMIME_PART (part));
     if (wrapper)
-       g_mime_data_wrapper_write_to_stream (wrapper, stream);
+       g_mime_data_wrapper_write_to_stream (wrapper, filter);
 
     g_object_unref (stream);
+    g_object_unref (filter);
+    g_object_unref (discard_uuencode_filter);
 
     g_byte_array_append (byte_array, (guint8 *) "\0", 1);
     body = (char *) g_byte_array_free (byte_array, FALSE);
index f0e905b70a339cc9c643710f313aa978c768e141..721c9a675a426c794999938ea62f1c009cb08ab3 100644 (file)
@@ -42,13 +42,6 @@ struct _notmuch_message {
     Xapian::Document doc;
 };
 
-/* "128 bits of thread-id ought to be enough for anybody" */
-#define NOTMUCH_THREAD_ID_BITS  128
-#define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4)
-typedef struct _thread_id {
-    char str[NOTMUCH_THREAD_ID_DIGITS + 1];
-} thread_id_t;
-
 /* We end up having to call the destructor explicitly because we had
  * to use "placement new" in order to initialize C++ objects within a
  * block that we allocated with talloc. So C++ is making talloc
@@ -434,7 +427,7 @@ notmuch_message_get_filename (notmuch_message_t *message)
     const char *prefix = _find_prefix ("file-direntry");
     int prefix_len = strlen (prefix);
     Xapian::TermIterator i;
-    char *direntry, *colon;
+    char *colon, *direntry = NULL;
     const char *db_path, *directory, *basename;
     unsigned int directory_id;
     void *local = talloc_new (message);
@@ -557,45 +550,6 @@ _notmuch_message_set_date (notmuch_message_t *message,
                            Xapian::sortable_serialise (time_value));
 }
 
-static void
-thread_id_generate (thread_id_t *thread_id)
-{
-    static int seeded = 0;
-    FILE *dev_random;
-    uint32_t value;
-    char *s;
-    int i;
-
-    if (! seeded) {
-       dev_random = fopen ("/dev/random", "r");
-       if (dev_random == NULL) {
-           srand (time (NULL));
-       } else {
-           fread ((void *) &value, sizeof (value), 1, dev_random);
-           srand (value);
-           fclose (dev_random);
-       }
-       seeded = 1;
-    }
-
-    s = thread_id->str;
-    for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) {
-       value = rand ();
-       sprintf (s, "%08x", value);
-       s += 8;
-    }
-}
-
-void
-_notmuch_message_ensure_thread_id (notmuch_message_t *message)
-{
-    /* If not part of any existing thread, generate a new thread_id. */
-    thread_id_t thread_id;
-
-    thread_id_generate (&thread_id);
-    _notmuch_message_add_term (message, "thread", thread_id.str);
-}
-
 /* Synchronize changes made to message->doc out into the database. */
 void
 _notmuch_message_sync (notmuch_message_t *message)
@@ -785,8 +739,8 @@ notmuch_message_remove_all_tags (notmuch_message_t *message)
        return status;
 
     for (tags = notmuch_message_get_tags (message);
-        notmuch_tags_has_more (tags);
-        notmuch_tags_advance (tags))
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags))
     {
        tag = notmuch_tags_get (tags);
 
index aa92535fa26ff3c3c750af43a5a2aab5aad543c4..db2b7a16eab2c7d715dbf75f4d8b839cb248546a 100644 (file)
@@ -102,13 +102,13 @@ _notmuch_messages_create (notmuch_message_list_t *list)
  *        anyway. *sigh*
  */
 notmuch_bool_t
-notmuch_messages_has_more (notmuch_messages_t *messages)
+notmuch_messages_valid (notmuch_messages_t *messages)
 {
     if (messages == NULL)
        return FALSE;
 
     if (! messages->is_of_list_type)
-       return _notmuch_mset_messages_has_more (messages);
+       return _notmuch_mset_messages_valid (messages);
 
     return (messages->iterator != NULL);
 }
@@ -126,10 +126,10 @@ notmuch_messages_get (notmuch_messages_t *messages)
 }
 
 void
-notmuch_messages_advance (notmuch_messages_t *messages)
+notmuch_messages_move_to_next (notmuch_messages_t *messages)
 {
     if (! messages->is_of_list_type)
-       return _notmuch_mset_messages_advance (messages);
+       return _notmuch_mset_messages_move_to_next (messages);
 
     if (messages->iterator == NULL)
        return;
@@ -162,11 +162,11 @@ notmuch_messages_collect_tags (notmuch_messages_t *messages)
        msg_tags = notmuch_message_get_tags (msg);
        while ((tag = notmuch_tags_get (msg_tags))) {
            g_hash_table_insert (htable, xstrdup (tag), NULL);
-           notmuch_tags_advance (msg_tags);
+           notmuch_tags_move_to_next (msg_tags);
        }
        notmuch_tags_destroy (msg_tags);
        notmuch_message_destroy (msg);
-       notmuch_messages_advance (messages);
+       notmuch_messages_move_to_next (messages);
     }
 
     keys = g_hash_table_get_keys (htable);
index c7fb0ef89312fa35ffda1cee7f22a9cd08da30c3..d52d84d41ad1055b379dba19a7100e09fcbbd823 100644 (file)
@@ -382,13 +382,13 @@ _notmuch_messages_create (notmuch_message_list_t *list);
 /* query.cc */
 
 notmuch_bool_t
-_notmuch_mset_messages_has_more (notmuch_messages_t *messages);
+_notmuch_mset_messages_valid (notmuch_messages_t *messages);
 
 notmuch_message_t *
 _notmuch_mset_messages_get (notmuch_messages_t *messages);
 
 void
-_notmuch_mset_messages_advance (notmuch_messages_t *messages);
+_notmuch_mset_messages_move_to_next (notmuch_messages_t *messages);
 
 /* message.cc */
 
index 15c9db40657d444006a2ec49e76c86503f528d06..88da07898eb20958154d80e5cfb72e054b6a2981 100644 (file)
@@ -247,8 +247,9 @@ notmuch_database_get_directory (notmuch_database_t *database,
  * 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. The new filename
- *     was successfully added to the message in the database.
+ *     ID as another message already in the database. The new
+ *     filename was successfully added to the message in the database
+ *     (if not already present).
  *
  * NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the
  *     file, (such as permission denied, or file not found,
@@ -327,8 +328,9 @@ notmuch_database_get_all_tags (notmuch_database_t *db);
  * As a special case, passing a length-zero string, (that is ""), will
  * result in a query that returns all messages in the database.
  *
- * See notmuch_query_set_sort for controlling the order of results and
- * notmuch_query_search to actually execute the query.
+ * See notmuch_query_set_sort for controlling the order of results.
+ * See notmuch_query_search_messages and notmuch_query_search_threads
+ * to actually execute the query.
  *
  * User should call notmuch_query_destroy when finished with this
  * query.
@@ -364,8 +366,8 @@ notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
  *     query = notmuch_query_create (database, query_string);
  *
  *     for (threads = notmuch_query_search_threads (query);
- *          notmuch_threads_has_more (threads);
- *          notmuch_threads_advance (threads))
+ *          notmuch_threads_valid (threads);
+ *          notmuch_threads_move_to_next (threads))
  *     {
  *         thread = notmuch_threads_get (threads);
  *         ....
@@ -403,8 +405,8 @@ notmuch_query_search_threads (notmuch_query_t *query);
  *     query = notmuch_query_create (database, query_string);
  *
  *     for (messages = notmuch_query_search_messages (query);
- *          notmuch_messages_has_more (messages);
- *          notmuch_messages_advance (messages))
+ *          notmuch_messages_valid (messages);
+ *          notmuch_messages_move_to_next (messages))
  *     {
  *         message = notmuch_messages_get (messages);
  *         ....
@@ -432,25 +434,24 @@ notmuch_query_search_messages (notmuch_query_t *query);
  *
  * This will in turn destroy any notmuch_threads_t and
  * notmuch_messages_t objects generated by this query, (and in
- * turn any notmuch_thrad_t and notmuch_message_t objects generated
+ * turn any notmuch_thread_t and notmuch_message_t objects generated
  * from those results, etc.), if such objects haven't already been
  * destroyed.
  */
 void
 notmuch_query_destroy (notmuch_query_t *query);
 
-/* Does the given notmuch_threads_t object contain any more
- * results.
+/* Is the given 'threads' iterator pointing at a valid thread.
  *
- * When this function returns TRUE, notmuch_threads_get will
- * return a valid object. Whereas when this function returns FALSE,
+ * When this function returns TRUE, notmuch_threads_get will return a
+ * valid object. Whereas when this function returns FALSE,
  * notmuch_threads_get will return NULL.
  *
  * See the documentation of notmuch_query_search_threads for example
  * code showing how to iterate over a notmuch_threads_t object.
  */
 notmuch_bool_t
-notmuch_threads_has_more (notmuch_threads_t *threads);
+notmuch_threads_valid (notmuch_threads_t *threads);
 
 /* Get the current thread from 'threads' as a notmuch_thread_t.
  *
@@ -466,13 +467,18 @@ notmuch_threads_has_more (notmuch_threads_t *threads);
 notmuch_thread_t *
 notmuch_threads_get (notmuch_threads_t *threads);
 
-/* Advance the 'threads' iterator to the next thread.
+/* Move the 'threads' iterator to the next thread.
+ *
+ * If 'threads' is already pointing at the last thread then the
+ * iterator will be moved to a point just beyond that last thread,
+ * (where notmuch_threads_valid will return FALSE and
+ * notmuch_threads_get will return NULL).
  *
  * See the documentation of notmuch_query_search_threads for example
  * code showing how to iterate over a notmuch_threads_t object.
  */
 void
-notmuch_threads_advance (notmuch_threads_t *threads);
+notmuch_threads_move_to_next (notmuch_threads_t *threads);
 
 /* Destroy a notmuch_threads_t object.
  *
@@ -566,7 +572,7 @@ notmuch_thread_get_subject (notmuch_thread_t *thread);
 time_t
 notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
 
-/* Get the date of the oldest message in 'thread' as a time_t value.
+/* Get the date of the newest message in 'thread' as a time_t value.
  */
 time_t
 notmuch_thread_get_newest_date (notmuch_thread_t *thread);
@@ -593,8 +599,8 @@ notmuch_thread_get_newest_date (notmuch_thread_t *thread);
  *     thread = notmuch_threads_get (threads);
  *
  *     for (tags = notmuch_thread_get_tags (thread);
- *          notmuch_tags_has_more (tags);
- *          notmuch_result_advance (tags))
+ *          notmuch_tags_valid (tags);
+ *          notmuch_result_move_to_next (tags))
  *     {
  *         tag = notmuch_tags_get (tags);
  *         ....
@@ -614,8 +620,7 @@ notmuch_thread_get_tags (notmuch_thread_t *thread);
 void
 notmuch_thread_destroy (notmuch_thread_t *thread);
 
-/* Does the given notmuch_messages_t object contain any more
- * messages.
+/* Is the given 'messages' iterator pointing at a valid message.
  *
  * When this function returns TRUE, notmuch_messages_get will return a
  * valid object. Whereas when this function returns FALSE,
@@ -625,7 +630,7 @@ notmuch_thread_destroy (notmuch_thread_t *thread);
  * code showing how to iterate over a notmuch_messages_t object.
  */
 notmuch_bool_t
-notmuch_messages_has_more (notmuch_messages_t *messages);
+notmuch_messages_valid (notmuch_messages_t *messages);
 
 /* Get the current message from 'messages' as a notmuch_message_t.
  *
@@ -641,13 +646,18 @@ notmuch_messages_has_more (notmuch_messages_t *messages);
 notmuch_message_t *
 notmuch_messages_get (notmuch_messages_t *messages);
 
-/* Advance the 'messages' iterator to the next result.
+/* Move the 'messages' iterator to the next message.
+ *
+ * If 'messages' is already pointing at the last message then the
+ * iterator will be moved to a point just beyond that last message,
+ * (where notmuch_messages_valid will return FALSE and
+ * notmuch_messages_get will return NULL).
  *
  * See the documentation of notmuch_query_search_messages for example
  * code showing how to iterate over a notmuch_messages_t object.
  */
 void
-notmuch_messages_advance (notmuch_messages_t *messages);
+notmuch_messages_move_to_next (notmuch_messages_t *messages);
 
 /* Destroy a notmuch_messages_t object.
  *
@@ -715,7 +725,7 @@ notmuch_message_get_thread_id (notmuch_message_t *message);
  * will return NULL.
  *
  * If there are no replies to 'message', this function will return
- * NULL. (Note that notmuch_messages_has_more will accept that NULL
+ * NULL. (Note that notmuch_messages_valid will accept that NULL
  * value as legitimate, and simply return FALSE for it.)
  */
 notmuch_messages_t *
@@ -792,8 +802,8 @@ notmuch_message_get_header (notmuch_message_t *message, const char *header);
  *     message = notmuch_database_find_message (database, message_id);
  *
  *     for (tags = notmuch_message_get_tags (message);
- *          notmuch_tags_has_more (tags);
- *          notmuch_result_advance (tags))
+ *          notmuch_tags_valid (tags);
+ *          notmuch_result_move_to_next (tags))
  *     {
  *         tag = notmuch_tags_get (tags);
  *         ....
@@ -864,7 +874,7 @@ notmuch_message_remove_all_tags (notmuch_message_t *message);
  * notmuch_message_remove_all_tags), will not be committed to the
  * database until the message is thawed with notmuch_message_thaw.
  *
- * Multiple calls to freeze/thaw are valid and these calls with
+ * Multiple calls to freeze/thaw are valid and these calls will
  * "stack". That is there must be as many calls to thaw as to freeze
  * before a message is actually thawed.
  *
@@ -882,7 +892,7 @@ notmuch_message_remove_all_tags (notmuch_message_t *message);
  *    notmuch_message_thaw (message);
  *
  * With freeze/thaw used like this, the message in the database is
- * guaranteed to have either the full set of original tag value, or
+ * guaranteed to have either the full set of original tag values, or
  * the full set of new tag values, but nothing in between.
  *
  * Imagine the example above without freeze/thaw and the operation
@@ -915,7 +925,7 @@ notmuch_message_freeze (notmuch_message_t *message);
  * NOTMUCH_STATUS_SUCCESS: Message successfully thawed, (or at least
  *     its frozen count has successfully been reduced by 1).
  *
- * NOTMUCH_STATUS_UNBALANCE_FREEZE_THAW: An attempt was made to thaw
+ * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: An attempt was made to thaw
  *     an unfrozen message. That is, there have been an unbalanced
  *     number of calls to notmuch_message_freeze and
  *     notmuch_message_thaw.
@@ -934,7 +944,7 @@ notmuch_message_thaw (notmuch_message_t *message);
 void
 notmuch_message_destroy (notmuch_message_t *message);
 
-/* Does the given notmuch_tags_t object contain any more tags.
+/* Is the given 'tags' iterator pointing at a valid tag.
  *
  * When this function returns TRUE, notmuch_tags_get will return a
  * valid string. Whereas when this function returns FALSE,
@@ -944,7 +954,7 @@ notmuch_message_destroy (notmuch_message_t *message);
  * showing how to iterate over a notmuch_tags_t object.
  */
 notmuch_bool_t
-notmuch_tags_has_more (notmuch_tags_t *tags);
+notmuch_tags_valid (notmuch_tags_t *tags);
 
 /* Get the current tag from 'tags' as a string.
  *
@@ -957,13 +967,18 @@ notmuch_tags_has_more (notmuch_tags_t *tags);
 const char *
 notmuch_tags_get (notmuch_tags_t *tags);
 
-/* Advance the 'tags' iterator to the next tag.
+/* Move the 'tags' iterator to the next tag.
+ *
+ * If 'tags' is already pointing at the last tag then the iterator
+ * will be moved to a point just beyond that last tag, (where
+ * notmuch_tags_valid will return FALSE and notmuch_tags_get will
+ * return NULL).
  *
  * See the documentation of notmuch_message_get_tags for example code
  * showing how to iterate over a notmuch_tags_t object.
  */
 void
-notmuch_tags_advance (notmuch_tags_t *tags);
+notmuch_tags_move_to_next (notmuch_tags_t *tags);
 
 /* Destroy a notmuch_tags_t object.
  *
@@ -1042,8 +1057,7 @@ notmuch_directory_get_child_directories (notmuch_directory_t *directory);
 void
 notmuch_directory_destroy (notmuch_directory_t *directory);
 
-/* Does the given notmuch_filenames_t object contain any more
- * filenames.
+/* Is the given 'filenames' iterator pointing at a valid filename.
  *
  * When this function returns TRUE, notmuch_filenames_get will return
  * a valid string. Whereas when this function returns FALSE,
@@ -1053,7 +1067,7 @@ notmuch_directory_destroy (notmuch_directory_t *directory);
  * function will always return FALSE.
  */
 notmuch_bool_t
-notmuch_filenames_has_more (notmuch_filenames_t *filenames);
+notmuch_filenames_valid (notmuch_filenames_t *filenames);
 
 /* Get the current filename from 'filenames' as a string.
  *
@@ -1066,13 +1080,18 @@ notmuch_filenames_has_more (notmuch_filenames_t *filenames);
 const char *
 notmuch_filenames_get (notmuch_filenames_t *filenames);
 
-/* Advance the 'filenames' iterator to the next filename.
+/* Move the 'filenames' iterator to the next filename.
+ *
+ * If 'filenames' is already pointing at the last filename then the
+ * iterator will be moved to a point just beyond that last filename,
+ * (where notmuch_filenames_valid will return FALSE and
+ * notmuch_filenames_get will return NULL).
  *
  * It is acceptable to pass NULL for 'filenames', in which case this
  * function will do nothing.
  */
 void
-notmuch_filenames_advance (notmuch_filenames_t *filenames);
+notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
 
 /* Destroy a notmuch_filenames_t object.
  *
index 2c8d167255bb903a5224d6d5b7ba0408e108bd00..9266d35f8fc517dbda11d3be9f68a0b47f7105da 100644 (file)
@@ -170,7 +170,7 @@ notmuch_query_search_messages (notmuch_query_t *query)
 }
 
 notmuch_bool_t
-_notmuch_mset_messages_has_more (notmuch_messages_t *messages)
+_notmuch_mset_messages_valid (notmuch_messages_t *messages)
 {
     notmuch_mset_messages_t *mset_messages;
 
@@ -189,7 +189,7 @@ _notmuch_mset_messages_get (notmuch_messages_t *messages)
 
     mset_messages = (notmuch_mset_messages_t *) messages;
 
-    if (! _notmuch_mset_messages_has_more (&mset_messages->base))
+    if (! _notmuch_mset_messages_valid (&mset_messages->base))
        return NULL;
 
     doc_id = *mset_messages->iterator;
@@ -208,7 +208,7 @@ _notmuch_mset_messages_get (notmuch_messages_t *messages)
 }
 
 void
-_notmuch_mset_messages_advance (notmuch_messages_t *messages)
+_notmuch_mset_messages_move_to_next (notmuch_messages_t *messages)
 {
     notmuch_mset_messages_t *mset_messages;
 
@@ -258,14 +258,14 @@ notmuch_query_destroy (notmuch_query_t *query)
 }
 
 notmuch_bool_t
-notmuch_threads_has_more (notmuch_threads_t *threads)
+notmuch_threads_valid (notmuch_threads_t *threads)
 {
     notmuch_message_t *message;
 
     if (threads->thread_id)
        return TRUE;
 
-    while (notmuch_messages_has_more (threads->messages))
+    while (notmuch_messages_valid (threads->messages))
     {
        message = notmuch_messages_get (threads->messages);
 
@@ -277,11 +277,11 @@ notmuch_threads_has_more (notmuch_threads_t *threads)
        {
            g_hash_table_insert (threads->threads,
                                 xstrdup (threads->thread_id), NULL);
-           notmuch_messages_advance (threads->messages);
+           notmuch_messages_move_to_next (threads->messages);
            return TRUE;
        }
 
-       notmuch_messages_advance (threads->messages);
+       notmuch_messages_move_to_next (threads->messages);
     }
 
     threads->thread_id = NULL;
@@ -291,7 +291,7 @@ notmuch_threads_has_more (notmuch_threads_t *threads)
 notmuch_thread_t *
 notmuch_threads_get (notmuch_threads_t *threads)
 {
-    if (! notmuch_threads_has_more (threads))
+    if (! notmuch_threads_valid (threads))
        return NULL;
 
     return _notmuch_thread_create (threads->query,
@@ -301,7 +301,7 @@ notmuch_threads_get (notmuch_threads_t *threads)
 }
 
 void
-notmuch_threads_advance (notmuch_threads_t *threads)
+notmuch_threads_move_to_next (notmuch_threads_t *threads)
 {
     threads->thread_id = NULL;
 }
index 85507e91fc35da13b52b1b82b2bcbe2a71af8f83..8fe4a3f0d4ebae5575f2e08abd5825781dbc1396 100644 (file)
@@ -77,7 +77,8 @@ _notmuch_tags_add_tag (notmuch_tags_t *tags, const char *tag)
  *
  * The internal creator of 'tags' should call this function before
  * returning 'tags' to the user to call the public functions such as
- * notmuch_tags_has_more, notmuch_tags_get, and notmuch_tags_advance. */
+ * notmuch_tags_valid, notmuch_tags_get, and
+ * notmuch_tags_move_to_next. */
 void
 _notmuch_tags_prepare_iterator (notmuch_tags_t *tags)
 {
@@ -89,7 +90,7 @@ _notmuch_tags_prepare_iterator (notmuch_tags_t *tags)
 }
 
 notmuch_bool_t
-notmuch_tags_has_more (notmuch_tags_t *tags)
+notmuch_tags_valid (notmuch_tags_t *tags)
 {
     return tags->iterator != NULL;
 }
@@ -104,7 +105,7 @@ notmuch_tags_get (notmuch_tags_t *tags)
 }
 
 void
-notmuch_tags_advance (notmuch_tags_t *tags)
+notmuch_tags_move_to_next (notmuch_tags_t *tags)
 {
     if (tags->iterator == NULL)
        return;
index 321937b0ac3315be803473debbc4d475736d7a23..1c8b39d208bc89a780675da6f27f98de0b168b97 100644 (file)
@@ -119,8 +119,8 @@ _thread_add_message (notmuch_thread_t *thread,
     }
 
     for (tags = notmuch_message_get_tags (message);
-        notmuch_tags_has_more (tags);
-        notmuch_tags_advance (tags))
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags))
     {
        tag = notmuch_tags_get (tags);
        g_hash_table_insert (thread->tags, xstrdup (tag), NULL);
@@ -269,8 +269,8 @@ _notmuch_thread_create (void *ctx,
     notmuch_query_set_sort (thread_id_query, NOTMUCH_SORT_OLDEST_FIRST);
 
     for (messages = notmuch_query_search_messages (thread_id_query);
-        notmuch_messages_has_more (messages);
-        notmuch_messages_advance (messages))
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
     {
        message = notmuch_messages_get (messages);
        _thread_add_message (thread, message);
@@ -278,10 +278,9 @@ _notmuch_thread_create (void *ctx,
     }
 
     notmuch_query_destroy (thread_id_query);
-
     for (messages = notmuch_query_search_messages (matched_query);
-        notmuch_messages_has_more (messages);
-        notmuch_messages_advance (messages))
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
     {
        message = notmuch_messages_get (messages);
        _thread_add_matched_message (thread, message);
index 77766de2cb56cda4bb8ac019426e16380c380839..d36b9ec16ce4aba89c782489b0ad91057fb34c8a 100644 (file)
@@ -51,6 +51,9 @@
 
 #define unused(x) x __attribute__ ((unused))
 
+#define STRINGIFY(s) STRINGIFY_(s)
+#define STRINGIFY_(s) #s
+
 /* 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).
@@ -107,6 +110,9 @@ notmuch_tag_command (void *ctx, int argc, char *argv[]);
 int
 notmuch_search_tags_command (void *ctx, int argc, char *argv[]);
 
+int
+notmuch_part_command (void *ctx, int argc, char *argv[]);
+
 const char *
 notmuch_time_relative_date (const void *ctx, time_t then);
 
@@ -123,6 +129,15 @@ notmuch_status_t
 show_message_body (const char *filename,
                   void (*show_part) (GMimeObject *part, int *part_count));
 
+notmuch_status_t
+show_one_part (const char *filename, int part);
+
+char *
+json_quote_chararray (const void *ctx, const char *str, const size_t len);
+
+char *
+json_quote_str (const void *ctx, const char *str);
+
 /* notmuch-config.c */
 
 typedef struct _notmuch_config notmuch_config_t;
index ea326bb672efb89a27ab33c62235c5a7f51ea008..7e7bc177ed4995a00c993b773a6630e39a97473c 100644 (file)
@@ -59,8 +59,8 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
     }
 
     for (messages = notmuch_query_search_messages (query);
-        notmuch_messages_has_more (messages);
-        notmuch_messages_advance (messages))
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
     {
        int first = 1;
        message = notmuch_messages_get (messages);
@@ -69,8 +69,8 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
                 "%s (", notmuch_message_get_message_id (message));
 
        for (tags = notmuch_message_get_tags (message);
-            notmuch_tags_has_more (tags);
-            notmuch_tags_advance (tags))
+            notmuch_tags_valid (tags);
+            notmuch_tags_move_to_next (tags))
        {
            if (! first)
                fprintf (output, " ");
index b740ee2b8c29d0e09d65f19de92693f2bcb448b3..44b50aaa2c49ceea06825abea39256790f2c1a5d 100644 (file)
@@ -153,7 +153,7 @@ _entries_resemble_maildir (struct dirent **entries, int count)
     int i, found = 0;
 
     for (i = 0; i < count; i++) {
-       if (entries[i]->d_type != DT_DIR)
+       if (entries[i]->d_type != DT_DIR && entries[i]->d_type != DT_UNKNOWN)
            continue;
 
        if (strcmp(entries[i]->d_name, "new") == 0 ||
@@ -273,8 +273,19 @@ add_files_recursive (notmuch_database_t *notmuch,
 
        entry = fs_entries[i];
 
-       if (entry->d_type != DT_DIR && entry->d_type != DT_LNK)
+       /* 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)
+       {
            continue;
+       }
 
        /* Ignore special directories to avoid infinite recursion.
         * Also ignore the .notmuch directory and any "tmp" directory
@@ -313,7 +324,7 @@ add_files_recursive (notmuch_database_t *notmuch,
 
        /* Check if we've walked past any names in db_files or
         * db_subdirs. If so, these have been deleted. */
-       while (notmuch_filenames_has_more (db_files) &&
+       while (notmuch_filenames_valid (db_files) &&
               strcmp (notmuch_filenames_get (db_files), entry->d_name) < 0)
        {
            char *absolute = talloc_asprintf (state->removed_files,
@@ -322,10 +333,10 @@ add_files_recursive (notmuch_database_t *notmuch,
 
            _filename_list_add (state->removed_files, absolute);
 
-           notmuch_filenames_advance (db_files);
+           notmuch_filenames_move_to_next (db_files);
        }
 
-       while (notmuch_filenames_has_more (db_subdirs) &&
+       while (notmuch_filenames_valid (db_subdirs) &&
               strcmp (notmuch_filenames_get (db_subdirs), entry->d_name) <= 0)
        {
            const char *filename = notmuch_filenames_get (db_subdirs);
@@ -338,12 +349,18 @@ add_files_recursive (notmuch_database_t *notmuch,
                _filename_list_add (state->removed_directories, absolute);
            }
 
-           notmuch_filenames_advance (db_subdirs);
+           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). */
-       if (entry->d_type == DT_LNK) {
+        * links to a regular file, (and not to a directory, say).
+        *
+        * Similarly, if the file is of unknown type (due to filesytem
+        * 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);
@@ -364,10 +381,10 @@ add_files_recursive (notmuch_database_t *notmuch,
        }
 
        /* Don't add a file that we've added before. */
-       if (notmuch_filenames_has_more (db_files) &&
+       if (notmuch_filenames_valid (db_files) &&
            strcmp (notmuch_filenames_get (db_files), entry->d_name) == 0)
        {
-           notmuch_filenames_advance (db_files);
+           notmuch_filenames_move_to_next (db_files);
            continue;
        }
 
@@ -439,7 +456,7 @@ add_files_recursive (notmuch_database_t *notmuch,
 
     /* Now that we've walked the whole filesystem list, anything left
      * over in the database lists has been deleted. */
-    while (notmuch_filenames_has_more (db_files))
+    while (notmuch_filenames_valid (db_files))
     {
        char *absolute = talloc_asprintf (state->removed_files,
                                          "%s/%s", path,
@@ -447,10 +464,10 @@ add_files_recursive (notmuch_database_t *notmuch,
 
        _filename_list_add (state->removed_files, absolute);
 
-       notmuch_filenames_advance (db_files);
+       notmuch_filenames_move_to_next (db_files);
     }
 
-    while (notmuch_filenames_has_more (db_subdirs))
+    while (notmuch_filenames_valid (db_subdirs))
     {
        char *absolute = talloc_asprintf (state->removed_directories,
                                          "%s/%s", path,
@@ -458,7 +475,7 @@ add_files_recursive (notmuch_database_t *notmuch,
 
        _filename_list_add (state->removed_directories, absolute);
 
-       notmuch_filenames_advance (db_subdirs);
+       notmuch_filenames_move_to_next (db_subdirs);
     }
 
     if (! interrupted) {
@@ -659,8 +676,8 @@ _remove_directory (void *ctx,
     directory = notmuch_database_get_directory (notmuch, path);
 
     for (files = notmuch_directory_get_child_files (directory);
-        notmuch_filenames_has_more (files);
-        notmuch_filenames_advance (files))
+        notmuch_filenames_valid (files);
+        notmuch_filenames_move_to_next (files))
     {
        absolute = talloc_asprintf (ctx, "%s/%s", path,
                                    notmuch_filenames_get (files));
@@ -673,8 +690,8 @@ _remove_directory (void *ctx,
     }
 
     for (subdirs = notmuch_directory_get_child_directories (directory);
-        notmuch_filenames_has_more (subdirs);
-        notmuch_filenames_advance (subdirs))
+        notmuch_filenames_valid (subdirs);
+        notmuch_filenames_move_to_next (subdirs))
     {
        absolute = talloc_asprintf (ctx, "%s/%s", path,
                                    notmuch_filenames_get (subdirs));
index 0cda72dcf05488be170faca9bd9a6e63f6c3e054..6c15536cff153d3d00ebe4d8a300c6361a9ea536 100644 (file)
 #include "notmuch-client.h"
 #include "gmime-filter-reply.h"
 
-static const struct {
-    const char *header;
-    const char *fallback;
-    GMimeRecipientType recipient_type;
-} reply_to_map[] = {
-    { "reply-to", "from", GMIME_RECIPIENT_TYPE_TO  },
-    { "to",         NULL, GMIME_RECIPIENT_TYPE_TO  },
-    { "cc",         NULL, GMIME_RECIPIENT_TYPE_CC  },
-    { "bcc",        NULL, GMIME_RECIPIENT_TYPE_BCC }
-};
-
 static void
 reply_part_content (GMimeObject *part)
 {
@@ -199,20 +188,113 @@ add_recipients_for_string (GMimeMessage *message,
     return add_recipients_for_address_list (message, config, type, list);
 }
 
+/* Does the address in the Reply-To header of 'message' already appear
+ * in either the 'To' or 'Cc' header of the message?
+ */
+static int
+reply_to_header_is_redundant (notmuch_message_t *message)
+{
+    const char *header, *addr;
+    InternetAddressList *list;
+    InternetAddress *address;
+    InternetAddressMailbox *mailbox;
+
+    header = notmuch_message_get_header (message, "reply-to");
+    if (*header == '\0')
+       return 0;
+
+    list = internet_address_list_parse_string (header);
+
+    if (internet_address_list_length (list) != 1)
+       return 0;
+
+    address = internet_address_list_get_address (list, 0);
+    if (INTERNET_ADDRESS_IS_GROUP (address))
+       return 0;
+
+    mailbox = INTERNET_ADDRESS_MAILBOX (address);
+    addr = internet_address_mailbox_get_addr (mailbox);
+
+    if (strstr (notmuch_message_get_header (message, "to"), addr) != 0 ||
+       strstr (notmuch_message_get_header (message, "cc"), addr) != 0)
+    {
+       return 1;
+    }
+
+    return 0;
+}
+
+/* Augments the recipients of reply from the headers of message.
+ *
+ * If any of the user's addresses were found in these headers, the first
+ * of these returned, otherwise NULL is returned.
+ */
+static const char *
+add_recipients_from_message (GMimeMessage *reply,
+                            notmuch_config_t *config,
+                            notmuch_message_t *message)
+{
+    struct {
+       const char *header;
+       const char *fallback;
+       GMimeRecipientType recipient_type;
+    } reply_to_map[] = {
+       { "reply-to", "from", GMIME_RECIPIENT_TYPE_TO  },
+       { "to",         NULL, GMIME_RECIPIENT_TYPE_TO  },
+       { "cc",         NULL, GMIME_RECIPIENT_TYPE_CC  },
+       { "bcc",        NULL, GMIME_RECIPIENT_TYPE_BCC }
+    };
+    const char *from_addr = NULL;
+    unsigned int i;
+
+    /* Some mailing lists munge the Reply-To header despite it being A Bad
+     * Thing, see http://www.unicom.com/pw/reply-to-harmful.html
+     *
+     * The munging is easy to detect, because it results in a
+     * redundant reply-to header, (with an address that already exists
+     * in either To or Cc). So in this case, we ignore the Reply-To
+     * field and use the From header. Thie ensures the original sender
+     * will get the reply even if not subscribed to the list. Note
+     * that the address in the Reply-To header will always appear in
+     * the reply.
+     */
+    if (reply_to_header_is_redundant (message)) {
+       reply_to_map[0].header = "from";
+       reply_to_map[0].fallback = NULL;
+    }
+
+    for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
+       const char *addr, *recipients;
+
+       recipients = notmuch_message_get_header (message,
+                                                reply_to_map[i].header);
+       if ((recipients == NULL || recipients[0] == '\0') && reply_to_map[i].fallback)
+           recipients = notmuch_message_get_header (message,
+                                                    reply_to_map[i].fallback);
+
+       addr = add_recipients_for_string (reply, config,
+                                         reply_to_map[i].recipient_type,
+                                         recipients);
+       if (from_addr == NULL)
+           from_addr = addr;
+    }
+
+    return from_addr;
+}
+
 static int
 notmuch_reply_format_default(void *ctx, notmuch_config_t *config, notmuch_query_t *query)
 {
     GMimeMessage *reply;
     notmuch_messages_t *messages;
     notmuch_message_t *message;
-    const char *subject, *recipients, *from_addr = NULL;
+    const char *subject, *from_addr = NULL;
     const char *in_reply_to, *orig_references, *references;
     char *reply_headers;
-    unsigned int i;
 
     for (messages = notmuch_query_search_messages (query);
-        notmuch_messages_has_more (messages);
-        notmuch_messages_advance (messages))
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
     {
        message = notmuch_messages_get (messages);
 
@@ -229,21 +311,7 @@ notmuch_reply_format_default(void *ctx, notmuch_config_t *config, notmuch_query_
            subject = talloc_asprintf (ctx, "Re: %s", subject);
        g_mime_message_set_subject (reply, subject);
 
-       for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
-           const char *addr;
-
-           recipients = notmuch_message_get_header (message,
-                                                    reply_to_map[i].header);
-           if ((recipients == NULL || recipients[0] == '\0') && reply_to_map[i].fallback)
-               recipients = notmuch_message_get_header (message,
-                                                        reply_to_map[i].fallback);
-
-           addr = add_recipients_for_string (reply, config,
-                                             reply_to_map[i].recipient_type,
-                                             recipients);
-           if (from_addr == NULL)
-               from_addr = addr;
-       }
+       from_addr = add_recipients_from_message (reply, config, message);
 
        if (from_addr == NULL)
            from_addr = notmuch_config_get_user_primary_email (config);
@@ -296,13 +364,12 @@ notmuch_reply_format_headers_only(void *ctx, notmuch_config_t *config, notmuch_q
     GMimeMessage *reply;
     notmuch_messages_t *messages;
     notmuch_message_t *message;
-    const char *recipients, *in_reply_to, *orig_references, *references;
+    const char *in_reply_to, *orig_references, *references;
     char *reply_headers;
-    unsigned int i;
 
     for (messages = notmuch_query_search_messages (query);
-        notmuch_messages_has_more (messages);
-        notmuch_messages_advance (messages))
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
     {
        message = notmuch_messages_get (messages);
 
@@ -332,19 +399,7 @@ notmuch_reply_format_headers_only(void *ctx, notmuch_config_t *config, notmuch_q
        g_mime_object_set_header (GMIME_OBJECT (reply),
                                  "References", references);
 
-       for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
-           const char *addr;
-
-           recipients = notmuch_message_get_header (message,
-                                                    reply_to_map[i].header);
-           if ((recipients == NULL || recipients[0] == '\0') && reply_to_map[i].fallback)
-               recipients = notmuch_message_get_header (message,
-                                                        reply_to_map[i].fallback);
-
-           addr = add_recipients_for_string (reply, config,
-                                             reply_to_map[i].recipient_type,
-                                             recipients);
-       }
+       (void)add_recipients_from_message (reply, config, message);
 
        g_mime_object_set_header (GMIME_OBJECT (reply), "Bcc",
                           notmuch_config_get_user_primary_email (config));
index 1b9598dcee5ec1dd42c075d1255781333ddc5de5..b0a4e1ce7905fc52157ce35c471fe1b31446f549 100644 (file)
@@ -63,9 +63,11 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
 
     while ((line_len = getline (&line, &line_size, input)) != -1) {
        regmatch_t match[3];
-       char *message_id, *tags, *tag, *next;
-       notmuch_message_t *message;
+       char *message_id, *file_tags, *tag, *next;
+       notmuch_message_t *message = NULL;
        notmuch_status_t status;
+       notmuch_tags_t *db_tags;
+       char *db_tags_str;
 
        chomp_newline (line);
 
@@ -79,8 +81,8 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
 
        message_id = xstrndup (line + match[1].rm_so,
                               match[1].rm_eo - match[1].rm_so);
-       tags = xstrndup (line + match[2].rm_so,
-                        match[2].rm_eo - match[2].rm_so);
+       file_tags = xstrndup (line + match[2].rm_so,
+                             match[2].rm_eo - match[2].rm_so);
 
        message = notmuch_database_find_message (notmuch, message_id);
        if (message == NULL) {
@@ -89,11 +91,30 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
            goto NEXT_LINE;
        }
 
-       notmuch_message_freeze (message);
+       db_tags_str = NULL;
+       for (db_tags = notmuch_message_get_tags (message);
+            notmuch_tags_valid (db_tags);
+            notmuch_tags_move_to_next (db_tags))
+       {
+           const char *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 (((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 NEXT_LINE;
+       }
 
+       notmuch_message_freeze (message);
        notmuch_message_remove_all_tags (message);
 
-       next = tags;
+       next = file_tags;
        while (next) {
            tag = strsep (&next, " ");
            if (*tag == '\0')
@@ -109,10 +130,13 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
        }
 
        notmuch_message_thaw (message);
-       notmuch_message_destroy (message);
+
       NEXT_LINE:
+       if (message)
+           notmuch_message_destroy (message);
+       message = NULL;
        free (message_id);
-       free (tags);
+       free (file_tags);
     }
 
     regfree (&regex);
index 7a1305e2fed98368ae42995a1bf3f28fcaae997e..6f3cfccbbf1bd2c80a113a69a3e2f4ce16881058 100644 (file)
@@ -28,7 +28,7 @@ print_tags (notmuch_tags_t *tags)
 
     while ((t = notmuch_tags_get (tags))) {
        printf ("%s\n", t);
-       notmuch_tags_advance (tags);
+       notmuch_tags_move_to_next (tags);
     }
 }
 
index dc44eb66bf4a249949dd25f004c7b98b85ff8912..4e3514b686467ef3d719ad8a79fc546ed3acd90b 100644 (file)
 
 #include "notmuch-client.h"
 
+typedef struct search_format {
+    const char *results_start;
+    const char *thread_start;
+    void (*thread) (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 *thread_sep;
+    const char *thread_end;
+    const char *results_end;
+} search_format_t;
+
+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_thread_text,
+           " (",
+               "%s", " ",
+           ")", "",
+       "\n",
+    "",
+};
+
+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);
+static const search_format_t format_json = {
+    "[",
+       "{",
+           format_thread_json,
+           "\"tags\": [",
+               "\"%s\"", ", ",
+           "]", ",\n",
+       "}",
+    "]\n",
+};
+
+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)
+{
+    printf ("thread:%s %12s [%d/%d] %s; %s",
+           thread_id,
+           notmuch_time_relative_date (ctx, date),
+           matched,
+           total,
+           authors,
+           subject);
+}
+
+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)
+{
+    struct tm *tm;
+    char timestamp[40];
+    void *ctx_quote = talloc_new (ctx);
+
+    tm = gmtime (&date);
+    if (tm == NULL)
+       INTERNAL_ERROR ("gmtime failed on thread %s.", thread_id);
+
+    if (strftime (timestamp, sizeof (timestamp), "%s", tm) == 0)
+       INTERNAL_ERROR ("strftime failed on thread %s.", thread_id);
+
+    printf ("\"thread\": %s,\n"
+           "\"timestamp\": %s,\n"
+           "\"matched\": %d,\n"
+           "\"total\": %d,\n"
+           "\"authors\": %s,\n"
+           "\"subject\": %s,\n",
+           json_quote_str (ctx_quote, thread_id),
+           timestamp,
+           matched,
+           total,
+           json_quote_str (ctx_quote, authors),
+           json_quote_str (ctx_quote, subject));
+
+    talloc_free (ctx_quote);
+}
+
 static void
 do_search_threads (const void *ctx,
+                  const search_format_t *format,
                   notmuch_query_t *query,
                   notmuch_sort_t sort)
 {
@@ -29,14 +141,19 @@ do_search_threads (const void *ctx,
     notmuch_threads_t *threads;
     notmuch_tags_t *tags;
     time_t date;
-    const char *relative_date;
+    int first_thread = 1;
+
+    fputs (format->results_start, stdout);
 
     for (threads = notmuch_query_search_threads (query);
-        notmuch_threads_has_more (threads);
-        notmuch_threads_advance (threads))
+        notmuch_threads_valid (threads);
+        notmuch_threads_move_to_next (threads))
     {
        int first_tag = 1;
 
+       if (! first_thread)
+           fputs (format->thread_sep, stdout);
+
        thread = notmuch_threads_get (threads);
 
        if (sort == NOTMUCH_SORT_OLDEST_FIRST)
@@ -44,30 +161,37 @@ do_search_threads (const void *ctx,
        else
            date = notmuch_thread_get_newest_date (thread);
 
-       relative_date = notmuch_time_relative_date (ctx, date);
+       fputs (format->thread_start, stdout);
+
+       format->thread (ctx,
+                       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));
 
-       printf ("thread:%s %12s [%d/%d] %s; %s",
-               notmuch_thread_get_thread_id (thread),
-               relative_date,
-               notmuch_thread_get_matched_messages (thread),
-               notmuch_thread_get_total_messages (thread),
-               notmuch_thread_get_authors (thread),
-               notmuch_thread_get_subject (thread));
+       fputs (format->tag_start, stdout);
 
-       printf (" (");
        for (tags = notmuch_thread_get_tags (thread);
-            notmuch_tags_has_more (tags);
-            notmuch_tags_advance (tags))
+            notmuch_tags_valid (tags);
+            notmuch_tags_move_to_next (tags))
        {
            if (! first_tag)
-               printf (" ");
-           printf ("%s", notmuch_tags_get (tags));
+               fputs (format->tag_sep, stdout);
+           printf (format->tag, notmuch_tags_get (tags));
            first_tag = 0;
        }
-       printf (")\n");
+
+       fputs (format->tag_end, stdout);
+       fputs (format->thread_end, stdout);
+
+       first_thread = 0;
 
        notmuch_thread_destroy (thread);
     }
+
+    fputs (format->results_end, stdout);
 }
 
 int
@@ -79,6 +203,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     char *query_str;
     char *opt;
     notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
+    const search_format_t *format = &format_text;
     int i;
 
     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
@@ -96,6 +221,16 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
                fprintf (stderr, "Invalid value for --sort: %s\n", opt);
                return 1;
            }
+       } else if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
+           opt = argv[i] + sizeof ("--format=") - 1;
+           if (strcmp (opt, "text") == 0) {
+               format = &format_text;
+           } else if (strcmp (opt, "json") == 0) {
+               format = &format_json;
+           } else {
+               fprintf (stderr, "Invalid value for --format: %s\n", opt);
+               return 1;
+           }
        } else {
            fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
            return 1;
@@ -132,7 +267,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     notmuch_query_set_sort (query, sort);
 
-    do_search_threads (ctx, query, sort);
+    do_search_threads (ctx, format, query, sort);
 
     notmuch_query_destroy (query);
     notmuch_database_close (notmuch);
index 376aacd7bcbe03be72d3562529f74d05d8efa1a3..76873a1d9036337868811659c99bb105ad371dfe 100644 (file)
 
 #include "notmuch-client.h"
 
+typedef struct show_format {
+    const char *message_set_start;
+    const char *message_start;
+    void (*message) (const void *ctx,
+                    notmuch_message_t *message,
+                    int indent);
+    const char *header_start;
+    void (*header) (const void *ctx,
+                   notmuch_message_t *message);
+    const char *header_end;
+    const char *body_start;
+    void (*part) (GMimeObject *part,
+                 int *part_count);
+    const char *body_end;
+    const char *message_end;
+    const char *message_set_sep;
+    const char *message_set_end;
+} show_format_t;
+
+static void
+format_message_text (unused (const void *ctx),
+                    notmuch_message_t *message,
+                    int indent);
+static void
+format_headers_text (const void *ctx,
+                    notmuch_message_t *message);
+static void
+format_part_text (GMimeObject *part,
+                 int *part_count);
+static const show_format_t format_text = {
+    "",
+       "\fmessage{ ", format_message_text,
+           "\fheader{\n", format_headers_text, "\fheader}\n",
+           "\fbody{\n", format_part_text, "\fbody}\n",
+       "\fmessage}\n", "",
+    ""
+};
+
+static void
+format_message_json (const void *ctx,
+                    notmuch_message_t *message,
+                    unused (int indent));
+static void
+format_headers_json (const void *ctx,
+                    notmuch_message_t *message);
+static void
+format_part_json (GMimeObject *part,
+                 int *part_count);
+static const show_format_t format_json = {
+    "[",
+       "{", format_message_json,
+           ", \"headers\": {", format_headers_json, "}",
+           ", \"body\": [", format_part_json, "]",
+       "}", ", ",
+    "]"
+};
+
 static const char *
-_get_tags_as_string (void *ctx, notmuch_message_t *message)
+_get_tags_as_string (const void *ctx, notmuch_message_t *message)
 {
     notmuch_tags_t *tags;
     int first = 1;
@@ -33,8 +90,8 @@ _get_tags_as_string (void *ctx, notmuch_message_t *message)
        return NULL;
 
     for (tags = notmuch_message_get_tags (message);
-        notmuch_tags_has_more (tags);
-        notmuch_tags_advance (tags))
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags))
     {
        tag = notmuch_tags_get (tags);
 
@@ -48,7 +105,7 @@ _get_tags_as_string (void *ctx, notmuch_message_t *message)
 
 /* Get a nice, single-line summary of message. */
 static const char *
-_get_one_line_summary (void *ctx, notmuch_message_t *message)
+_get_one_line_summary (const void *ctx, notmuch_message_t *message)
 {
     const char *from;
     time_t date;
@@ -67,18 +124,104 @@ _get_one_line_summary (void *ctx, notmuch_message_t *message)
 }
 
 static void
-show_part_content (GMimeObject *part)
+format_message_text (unused (const void *ctx), notmuch_message_t *message, int indent)
+{
+    printf ("id:%s depth:%d match:%d filename:%s\n",
+           notmuch_message_get_message_id (message),
+           indent,
+           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
+           notmuch_message_get_filename (message));
+}
+
+static void
+format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))
+{
+    notmuch_tags_t *tags;
+    int first = 1;
+    void *ctx_quote = talloc_new (ctx);
+    time_t date;
+    const char *relative_date;
+
+    date = notmuch_message_get_date (message);
+    relative_date = notmuch_time_relative_date (ctx, date);
+
+    printf ("\"id\": %s, \"match\": %s, \"filename\": %s, \"date_unix\": %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",
+           json_quote_str (ctx_quote, notmuch_message_get_filename (message)),
+           date, relative_date);
+
+    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);
+}
+
+static void
+format_headers_text (const void *ctx, notmuch_message_t *message)
+{
+    const char *headers[] = {
+       "Subject", "From", "To", "Cc", "Bcc", "Date"
+    };
+    const char *name, *value;
+    unsigned int i;
+
+    printf ("%s\n", _get_one_line_summary (ctx, message));
+
+    for (i = 0; i < ARRAY_SIZE (headers); i++) {
+       name = headers[i];
+       value = notmuch_message_get_header (message, name);
+       if (value && strlen (value))
+           printf ("%s: %s\n", name, value);
+    }
+}
+
+static void
+format_headers_json (const void *ctx, notmuch_message_t *message)
+{
+    const char *headers[] = {
+       "Subject", "From", "To", "Cc", "Bcc", "Date"
+    };
+    const char *name, *value;
+    unsigned int i;
+    int first_header = 1;
+    void *ctx_quote = talloc_new (ctx);
+
+    for (i = 0; i < ARRAY_SIZE (headers); i++) {
+       name = headers[i];
+       value = notmuch_message_get_header (message, name);
+       if (value)
+       {
+           if (!first_header)
+               fputs (", ", stdout);
+           first_header = 0;
+
+           printf ("%s: %s",
+                   json_quote_str (ctx_quote, name),
+                   json_quote_str (ctx_quote, value));
+       }
+    }
+
+    talloc_free (ctx_quote);
+}
+
+static void
+show_part_content (GMimeObject *part, GMimeStream *stream_out)
 {
-    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
     GMimeStream *stream_filter = NULL;
     GMimeDataWrapper *wrapper;
     const char *charset;
 
     charset = g_mime_object_get_content_type_parameter (part, "charset");
 
-    if (stream_stdout) {
-       g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
-       stream_filter = g_mime_stream_filter_new(stream_stdout);
+    if (stream_out) {
+       stream_filter = g_mime_stream_filter_new(stream_out);
        g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
                                 g_mime_filter_crlf_new(FALSE, FALSE));
         if (charset) {
@@ -92,12 +235,10 @@ show_part_content (GMimeObject *part)
        g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
     if (stream_filter)
        g_object_unref(stream_filter);
-    if (stream_stdout)
-       g_object_unref(stream_stdout);
 }
 
 static void
-show_part (GMimeObject *part, int *part_count)
+format_part_text (GMimeObject *part, int *part_count)
 {
     GMimeContentDisposition *disposition;
     GMimeContentType *content_type;
@@ -118,7 +259,10 @@ show_part (GMimeObject *part, int *part_count)
        if (g_mime_content_type_is_type (content_type, "text", "*") &&
            !g_mime_content_type_is_type (content_type, "text", "html"))
        {
-           show_part_content (part);
+           GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
+           g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+           show_part_content (part, stream_stdout);
+           g_object_unref(stream_stdout);
        }
 
        printf ("\fattachment}\n");
@@ -135,7 +279,10 @@ show_part (GMimeObject *part, int *part_count)
     if (g_mime_content_type_is_type (content_type, "text", "*") &&
        !g_mime_content_type_is_type (content_type, "text", "html"))
     {
-       show_part_content (part);
+       GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
+       g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+       show_part_content (part, stream_stdout);
+       g_object_unref(stream_stdout);
     }
     else
     {
@@ -147,54 +294,90 @@ show_part (GMimeObject *part, int *part_count)
 }
 
 static void
-show_message (void *ctx, notmuch_message_t *message, int indent)
+format_part_json (GMimeObject *part, int *part_count)
 {
-    const char *headers[] = {
-       "Subject", "From", "To", "Cc", "Bcc", "Date"
-    };
-    const char *name, *value;
-    unsigned int i;
+    GMimeContentType *content_type;
+    GMimeContentDisposition *disposition;
+    void *ctx = talloc_new (NULL);
+    GMimeStream *stream_memory = g_mime_stream_mem_new ();
+    GByteArray *part_content;
 
-    printf ("\fmessage{ id:%s depth:%d match:%d filename:%s\n",
-           notmuch_message_get_message_id (message),
-           indent,
-           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
-           notmuch_message_get_filename (message));
+    content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
 
-    printf ("\fheader{\n");
+    if (*part_count > 1)
+       fputs (", ", stdout);
 
-    printf ("%s\n", _get_one_line_summary (ctx, message));
+    printf ("{\"id\": %d, \"content-type\": %s",
+           *part_count,
+           json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
 
-    for (i = 0; i < ARRAY_SIZE (headers); i++) {
-       name = headers[i];
-       value = notmuch_message_get_header (message, name);
-       if (value)
-           printf ("%s: %s\n", name, value);
+    disposition = g_mime_object_get_content_disposition (part);
+    if (disposition &&
+       strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+    {
+       const char *filename = g_mime_part_get_filename (GMIME_PART (part));
+
+       printf (", \"filename\": %s", json_quote_str (ctx, filename));
     }
 
-    printf ("\fheader}\n");
-    printf ("\fbody{\n");
+    if (g_mime_content_type_is_type (content_type, "text", "*") &&
+       !g_mime_content_type_is_type (content_type, "text", "html"))
+    {
+       show_part_content (part, stream_memory);
+       part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
 
-    show_message_body (notmuch_message_get_filename (message), show_part);
+       printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
+    }
 
-    printf ("\fbody}\n");
+    fputs ("}", stdout);
 
-    printf ("\fmessage}\n");
+    talloc_free (ctx);
+    if (stream_memory)
+       g_object_unref (stream_memory);
+}
+
+static void
+show_message (void *ctx, const show_format_t *format, notmuch_message_t *message, int indent)
+{
+    fputs (format->message_start, stdout);
+    if (format->message)
+       format->message(ctx, message, indent);
+
+    fputs (format->header_start, stdout);
+    if (format->header)
+       format->header(ctx, message);
+    fputs (format->header_end, stdout);
+
+    fputs (format->body_start, stdout);
+    if (format->part)
+       show_message_body (notmuch_message_get_filename (message), format->part);
+    fputs (format->body_end, stdout);
+
+    fputs (format->message_end, stdout);
 }
 
 
 static void
-show_messages (void *ctx, notmuch_messages_t *messages, int indent,
+show_messages (void *ctx, const show_format_t *format, notmuch_messages_t *messages, int indent,
               notmuch_bool_t entire_thread)
 {
     notmuch_message_t *message;
     notmuch_bool_t match;
+    int first_set = 1;
     int next_indent;
 
+    fputs (format->message_set_start, stdout);
+
     for (;
-        notmuch_messages_has_more (messages);
-        notmuch_messages_advance (messages))
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
     {
+       if (!first_set)
+           fputs (format->message_set_sep, stdout);
+       first_set = 0;
+
+       fputs (format->message_set_start, stdout);
+
        message = notmuch_messages_get (messages);
 
        match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
@@ -202,15 +385,21 @@ show_messages (void *ctx, notmuch_messages_t *messages, int indent,
        next_indent = indent;
 
        if (match || entire_thread) {
-           show_message (ctx, message, indent);
+           show_message (ctx, format, message, indent);
            next_indent = indent + 1;
+
+           fputs (format->message_set_sep, stdout);
        }
 
-       show_messages (ctx, notmuch_message_get_replies (message),
+       show_messages (ctx, format, notmuch_message_get_replies (message),
                       next_indent, entire_thread);
 
        notmuch_message_destroy (message);
+
+       fputs (format->message_set_end, stdout);
     }
+
+    fputs (format->message_set_end, stdout);
 }
 
 int
@@ -223,15 +412,29 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
     notmuch_thread_t *thread;
     notmuch_messages_t *messages;
     char *query_string;
+    char *opt;
+    const show_format_t *format = &format_text;
     int entire_thread = 0;
     int i;
+    int first_toplevel = 1;
 
     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
        if (strcmp (argv[i], "--") == 0) {
            i++;
            break;
        }
-        if (strcmp(argv[i], "--entire-thread") == 0) {
+       if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
+           opt = argv[i] + sizeof ("--format=") - 1;
+           if (strcmp (opt, "text") == 0) {
+               format = &format_text;
+           } else if (strcmp (opt, "json") == 0) {
+               format = &format_json;
+               entire_thread = 1;
+           } else {
+               fprintf (stderr, "Invalid value for --format: %s\n", opt);
+               return 1;
+           }
+       } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
            entire_thread = 1;
        } else {
            fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
@@ -268,9 +471,11 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        return 1;
     }
 
+    fputs (format->message_set_start, stdout);
+
     for (threads = notmuch_query_search_threads (query);
-        notmuch_threads_has_more (threads);
-        notmuch_threads_advance (threads))
+        notmuch_threads_valid (threads);
+        notmuch_threads_move_to_next (threads))
     {
        thread = notmuch_threads_get (threads);
 
@@ -280,13 +485,95 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
            INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
                            notmuch_thread_get_thread_id (thread));
 
-       show_messages (ctx, messages, 0, entire_thread);
+       if (!first_toplevel)
+           fputs (format->message_set_sep, stdout);
+       first_toplevel = 0;
+
+       show_messages (ctx, format, messages, 0, entire_thread);
 
        notmuch_thread_destroy (thread);
+
     }
 
+    fputs (format->message_set_end, stdout);
+
     notmuch_query_destroy (query);
     notmuch_database_close (notmuch);
 
     return 0;
 }
+
+int
+notmuch_part_command (void *ctx, unused (int argc), unused (char *argv[]))
+{
+       notmuch_config_t *config;
+       notmuch_database_t *notmuch;
+       notmuch_query_t *query;
+       notmuch_messages_t *messages;
+       notmuch_message_t *message;
+       char *query_string;
+       int i;
+       int part = 0;
+
+       for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+               if (strcmp (argv[i], "--") == 0) {
+                       i++;
+                       break;
+               }
+               if (STRNCMP_LITERAL (argv[i], "--part=") == 0) {
+                       part = atoi(argv[i] + sizeof ("--part=") - 1);
+               } else {
+                       fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+                       return 1;
+               }
+       }
+
+       argc -= i;
+       argv += i;
+
+       config = notmuch_config_open (ctx, NULL, NULL);
+       if (config == NULL)
+               return 1;
+
+       query_string = query_string_from_args (ctx, argc, argv);
+       if (query_string == NULL) {
+               fprintf (stderr, "Out of memory\n");
+               return 1;
+       }
+
+       if (*query_string == '\0') {
+               fprintf (stderr, "Error: notmuch part requires at least one search term.\n");
+               return 1;
+       }
+
+       notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+                                        NOTMUCH_DATABASE_MODE_READ_ONLY);
+       if (notmuch == NULL)
+               return 1;
+
+       query = notmuch_query_create (notmuch, query_string);
+       if (query == NULL) {
+               fprintf (stderr, "Out of memory\n");
+               return 1;
+       }
+
+       if (notmuch_query_count_messages (query) != 1) {
+               fprintf (stderr, "Error: search term did not match precisely one message.\n");
+               return 1;
+       }
+
+       messages = notmuch_query_search_messages (query);
+       message = notmuch_messages_get (messages);
+
+       if (message == NULL) {
+               fprintf (stderr, "Error: cannot find matching message.\n");
+               return 1;
+       }
+
+       show_one_part (notmuch_message_get_filename (message), part);
+
+       notmuch_query_destroy (query);
+       notmuch_database_close (notmuch);
+
+       return 0;
+}
index 00588a11608bc0b5a8ead8d7f7e4a2bb804d403b..8b6f7dc081f4dcd79cba27539ccc0dca9fdfc246 100644 (file)
@@ -108,8 +108,8 @@ notmuch_tag_command (void *ctx, unused (int argc), unused (char *argv[]))
     }
 
     for (messages = notmuch_query_search_messages (query);
-        notmuch_messages_has_more (messages) && !interrupted;
-        notmuch_messages_advance (messages))
+        notmuch_messages_valid (messages) && !interrupted;
+        notmuch_messages_move_to_next (messages))
     {
        message = notmuch_messages_get (messages);
 
index 282ad9896bc6a64004f0c4b16fed2e15ba00b48a..9d0473d5941423b310bf3c0af9389c2d49399064 100644 (file)
--- a/notmuch.1
+++ b/notmuch.1
@@ -146,6 +146,12 @@ Supported options for
 include
 .RS 4
 .TP 4
+.BR \-\-format= ( json | text )
+
+Presents the results in either JSON or plain-text (default).
+.RE
+.RS 4
+.TP 4
 .BR \-\-sort= ( newest\-first | oldest\-first )
 
 This option can be used to present results in either chronological order
@@ -194,7 +200,14 @@ matched message will be displayed.
 .RE
 
 .RS 4
-The  output format  is plain-text,  with all  text-content  MIME parts
+.TP 4
+.B \-\-format=(json|text)
+
+.RS 4
+.TP 4
+.B text
+
+The default plain-text format  has  text-content  MIME parts
 decoded. Various components in the output,
 .RB ( message ", " header ", " body ", " attachment ", and MIME " part ),
 will be delimited by easily-parsed markers. Each marker consists of a
@@ -202,6 +215,18 @@ Control-L character (ASCII decimal 12), the name of the marker, and
 then either an opening or closing brace, ('{' or '}'), to either open
 or close the component.
 
+.RE
+.RS 4
+.TP 4
+.B json
+
+Format output as Javascript Object Notation (JSON). JSON output always
+includes all messages in a matching thread; in effect
+.B \-\-format=json
+implies
+.B \-\-entire\-thread
+
+.RE
 A common use of
 .B notmuch show
 is to display a single thread of email messages. For this, use a
@@ -328,6 +353,32 @@ 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).
+.RE
+
+The
+.B part
+command can used to output a single part of a multi-part MIME message.
+
+.RS 4
+.TP 4
+.BR part " --part=<part-number> <search-term>..."
+
+Output a single MIME part of a message.
+
+A single decoded MIME part, with no encoding or framing, is output to
+stdout. The search terms must match only a single message, otherwise
+this command will fail.
+
+The part number should match the part "id" field output by the
+"--format=json" option of "notmuch show". If the message specified by
+the search terms does not include a part with the specified "id" there
+will be no output.
+
+See the
+.B "SEARCH SYNTAX"
+section below for details of the supported syntax for <search-terms>.
+.RE
+
 .SH SEARCH SYNTAX
 Several notmuch commands accept a common syntax for search terms.
 
@@ -348,7 +399,7 @@ terms to match against specific portions of an email, (where
 
        attachment:<word>
 
-       tag:<tag>
+       tag:<tag> (or is:<tag>)
 
        id:<message-id>
 
@@ -377,7 +428,7 @@ prefix can be used to search for specific filenames (or extensions) of
 attachments to email messages.
 
 For
-.BR tag: ,
+.BR tag: " and " is:
 valid tag values include
 .BR inbox " and " unread
 by default for new messages added by
@@ -424,6 +475,13 @@ specify a date range to return messages from 2009-10-01 until the
 current time:
 
        $(date +%s -d 2009-10-01)..$(date +%s)
+.SH ENVIRONMENT
+The following environment variables can be used to control the
+behavior of notmuch.
+.TP
+.B NOTMUCH_CONFIG
+Specifies the location of the notmuch configuration file. Notmuch will
+use ${HOME}/.notmuch-config if this variable is not set.
 .SH SEE ALSO
 The emacs-based interface to notmuch (available as
 .B notmuch.el
index 87479f81056b53cef82859c61adb7794f19ecf79..f5669fcda2093cf5116f7bb99e7c5674745eb9d3 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -36,254 +36,280 @@ static int
 notmuch_help_command (void *ctx, int argc, char *argv[]);
 
 static const char search_terms_help[] =
-    "\t\tSeveral notmuch commands accept a comman syntax for search\n"
-    "\t\tterms.\n"
+    "\tSeveral notmuch commands accept a comman syntax for search\n"
+    "\tterms.\n"
     "\n"
-    "\t\tThe search terms can consist of free-form text (and quoted\n"
-    "\t\tphrases) which will match all messages that contain all of\n"
-    "\t\tthe given terms/phrases in the body, the subject, or any of\n"
-    "\t\tthe sender or recipient headers.\n"
+    "\tThe search terms can consist of free-form text (and quoted\n"
+    "\tphrases) which will match all messages that contain all of\n"
+    "\tthe given terms/phrases in the body, the subject, or any of\n"
+    "\tthe sender or recipient headers.\n"
     "\n"
-    "\t\tIn addition to free text, the following prefixes can be used\n"
-    "\t\tto force terms to match against specific portions of an email,\n"
-    "\t\t(where <brackets> indicate user-supplied values):\n"
+    "\tIn addition to free text, the following prefixes can be used\n"
+    "\tto force terms to match against specific portions of an email,\n"
+    "\t(where <brackets> indicate user-supplied values):\n"
     "\n"
-    "\t\t\tfrom:<name-or-address>\n"
-    "\t\t\tto:<name-or-address>\n"
-    "\t\t\tsubject:<word-or-quoted-phrase>\n"
-    "\t\t\tattachment:<word>\n"
-    "\t\t\ttag:<tag>\n"
-    "\t\t\tid:<message-id>\n"
-    "\t\t\tthread:<thread-id>\n"
+    "\t\tfrom:<name-or-address>\n"
+    "\t\tto:<name-or-address>\n"
+    "\t\tsubject:<word-or-quoted-phrase>\n"
+    "\t\tattachment:<word>\n"
+    "\t\ttag:<tag> (or is:<tag>)\n"
+    "\t\tid:<message-id>\n"
+    "\t\tthread:<thread-id>\n"
     "\n"
-    "\t\tThe from: prefix is used to match the name or address of\n"
-    "\t\tthe sender of an email message.\n"
+    "\tThe from: prefix is used to match the name or address of\n"
+    "\tthe sender of an email message.\n"
     "\n"
-    "\t\tThe to: prefix is used to match the names or addresses of\n"
-    "\t\tany recipient of an email message, (whether To, Cc, or Bcc).\n"
+    "\tThe to: prefix is used to match the names or addresses of\n"
+    "\tany recipient of an email message, (whether To, Cc, or Bcc).\n"
     "\n"
-    "\t\tAny term prefixed with subject: will match only text from\n"
-    "\t\tthe subject of an email. Quoted phrases are supported when\n"
-    "\t\tsearching with: subject:\"this is a phrase\".\n"
+    "\tAny term prefixed with subject: will match only text from\n"
+    "\tthe subject of an email. Quoted phrases are supported when\n"
+    "\tsearching with: subject:\"this is a phrase\".\n"
     "\n"
-    "\t\tFor tag:, valid tag values include \"inbox\" and \"unread\"\n"
-    "\t\tby default for new messages added by \"notmuch new\" as well\n"
-    "\t\tas any other tag values added manually with \"notmuch tag\".\n"
+    "\tFor tag: and is:, valid tag values include \"inbox\" and \"unread\"\n"
+    "\tby default for new messages added by \"notmuch new\" as well\n"
+    "\tas any other tag values added manually with \"notmuch tag\".\n"
     "\n"
-    "\t\tFor id:, message ID values are the literal contents of the\n"
-    "\t\tMessage-ID: header of email messages, but without the '<','>'\n"
-    "\t\tdelimiters.\n"
+    "\tFor id:, message ID values are the literal contents of the\n"
+    "\tMessage-ID: header of email messages, but without the '<','>'\n"
+    "\tdelimiters.\n"
     "\n"
-    "\t\tThe thread: prefix can be used with the thread ID values that\n"
-    "\t\tare generated internally by notmuch (and do not appear in email\n"
-    "\t\tmessages). These thread ID values can be seen in the first\n"
-    "\t\tcolumn of output from \"notmuch search\".\n"
+    "\tThe thread: prefix can be used with the thread ID values that\n"
+    "\tare generated internally by notmuch (and do not appear in email\n"
+    "\tmessages). These thread ID values can be seen in the first\n"
+    "\tcolumn of output from \"notmuch search\".\n"
     "\n"
-    "\t\tIn addition to individual terms, multiple terms can be\n"
-    "\t\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
-    "\t\tEach term in the query will be implicitly connected by a\n"
-    "\t\tlogical AND if no explicit operator is provided, (except\n"
-    "\t\tthat terms with a common prefix will be implicitly combined\n"
-    "\t\twith OR until we get Xapian defect #402 fixed).\n"
+    "\tIn addition to individual terms, multiple terms can be\n"
+    "\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
+    "\tEach term in the query will be implicitly connected by a\n"
+    "\tlogical AND if no explicit operator is provided, (except\n"
+    "\tthat terms with a common prefix will be implicitly combined\n"
+    "\twith OR until we get Xapian defect #402 fixed).\n"
     "\n"
-    "\t\tParentheses can also be used to control the combination of\n"
-    "\t\tthe Boolean operators, but will have to be protected from\n"
-    "\t\tinterpretation by the shell, (such as by putting quotation\n"
-    "\t\tmarks around any parenthesized expression).\n"
+    "\tParentheses can also be used to control the combination of\n"
+    "\tthe Boolean operators, but will have to be protected from\n"
+    "\tinterpretation by the shell, (such as by putting quotation\n"
+    "\tmarks around any parenthesized expression).\n"
     "\n"
-    "\t\tFinally, results can be restricted to only messages within a\n"
-    "\t\tparticular time range, (based on the Date: header) with:\n"
+    "\tFinally, results can be restricted to only messages within a\n"
+    "\tparticular time range, (based on the Date: header) with:\n"
     "\n"
-    "\t\t\t<intial-timestamp>..<final-timestamp>\n"
+    "\t\t<intial-timestamp>..<final-timestamp>\n"
     "\n"
-    "\t\tEach timestamp is a number representing the number of seconds\n"
-    "\t\tsince 1970-01-01 00:00:00 UTC. This is not the most convenient\n"
-    "\t\tmeans of expressing date ranges, but until notmuch is fixed to\n"
-    "\t\taccept a more convenient form, one can use the date program to\n"
-    "\t\tconstruct timestamps. For example, with the bash shell the\n"
-    "\t\tfollowing syntax would specify a date range to return messages\n"
-    "\t\tfrom 2009-10-01 until the current time:\n"
+    "\tEach timestamp is a number representing the number of seconds\n"
+    "\tsince 1970-01-01 00:00:00 UTC. This is not the most convenient\n"
+    "\tmeans of expressing date ranges, but until notmuch is fixed to\n"
+    "\taccept a more convenient form, one can use the date program to\n"
+    "\tconstruct timestamps. For example, with the bash shell the\n"
+    "\tfollowing syntax would specify a date range to return messages\n"
+    "\tfrom 2009-10-01 until the current time:\n"
     "\n"
-    "\t\t\t$(date +%%s -d 2009-10-01)..$(date +%%s)\n\n";
+    "\t\t$(date +%%s -d 2009-10-01)..$(date +%%s)\n\n";
 
 command_t commands[] = {
     { "setup", notmuch_setup_command,
       NULL,
       "Interactively setup notmuch for first use.",
-      "\t\tThe setup command will prompt for your full name, your primary\n"
-      "\t\temail address, any alternate email addresses you use, and the\n"
-      "\t\tdirectory containing your email archives. Your answers will be\n"
-      "\t\twritten to a configuration file in ${NOTMUCH_CONFIG} (if set)\n"
-      "\t\tor ${HOME}/.notmuch-config.\n"
-      "\n"
-      "\t\tThis configuration file will be created with descriptive\n"
-      "\t\tcomments, making it easy to edit by hand later to change the\n"
-      "\t\tconfiguration. Or you can run \"notmuch setup\" again.\n"
-      "\n"
-      "\t\tInvoking notmuch with no command argument will run setup if\n"
-      "\t\tthe setup command has not previously been completed." },
+      "\tThe setup command will prompt for your full name, your primary\n"
+      "\temail address, any alternate email addresses you use, and the\n"
+      "\tdirectory containing your email archives. Your answers will be\n"
+      "\twritten to a configuration file in ${NOTMUCH_CONFIG} (if set)\n"
+      "\tor ${HOME}/.notmuch-config.\n"
+      "\n"
+      "\tThis configuration file will be created with descriptive\n"
+      "\tcomments, making it easy to edit by hand later to change the\n"
+      "\tconfiguration. Or you can run \"notmuch setup\" again.\n"
+      "\n"
+      "\tInvoking notmuch with no command argument will run setup if\n"
+      "\tthe setup command has not previously been completed." },
     { "new", notmuch_new_command,
       "[--verbose]",
-      "\t\tFind and import new messages to the notmuch database.",
-      "\t\tScans all sub-directories of the mail directory, performing\n"
-      "\t\tfull-text indexing on new messages that are found. Each new\n"
-      "\t\tmessage will be tagged as both \"inbox\" and \"unread\".\n"
+      "Find and import new messages to the notmuch database.",
+      "\tScans all sub-directories of the mail directory, performing\n"
+      "\tfull-text indexing on new messages that are found. Each new\n"
+      "\tmessage will be tagged as both \"inbox\" and \"unread\".\n"
       "\n"
-      "\t\tYou should run \"notmuch new\" once after first running\n"
-      "\t\t\"notmuch setup\" to create the initial database. The first\n"
-      "\t\trun may take a long time if you have a significant amount of\n"
-      "\t\tmail (several hundred thousand messages or more).\n"
+      "\tYou should run \"notmuch new\" once after first running\n"
+      "\t\"notmuch setup\" to create the initial database. The first\n"
+      "\trun may take a long time if you have a significant amount of\n"
+      "\tmail (several hundred thousand messages or more).\n"
       "\n"
-      "\t\tSubsequently, you should run \"notmuch new\" whenever new mail\n"
-      "\t\tis delivered and you wish to incorporate it into the database.\n"
-      "\t\tThese subsequent runs will be much quicker than the initial run.\n"
+      "\tSubsequently, you should run \"notmuch new\" whenever new mail\n"
+      "\tis delivered and you wish to incorporate it into the database.\n"
+      "\tThese subsequent runs will be much quicker than the initial run.\n"
       "\n"
-      "\t\tSupported options for new include:\n"
+      "\tSupported options for new include:\n"
       "\n"
-      "\t\t--verbose\n"
+      "\t--verbose\n"
       "\n"
-      "\t\t\tVerbose operation. Shows paths of message files as\n"
-      "\t\t\tthey are being indexed.\n"
+      "\t\tVerbose operation. Shows paths of message files as\n"
+      "\t\tthey are being indexed.\n"
       "\n"
-      "\t\tInvoking notmuch with no command argument will run new if\n"
-      "\t\tthe setup command has previously been completed, but new has\n"
-      "\t\tnot previously been run." },
+      "\tInvoking notmuch with no command argument will run new if\n"
+      "\tthe setup command has previously been completed, but new has\n"
+      "\tnot previously been run." },
     { "search", notmuch_search_command,
       "[options...] <search-terms> [...]",
-      "\t\tSearch for messages matching the given search terms.",
-      "\t\tNote that the individual mail messages will be matched\n"
-      "\t\tagainst the search terms, but the results will be the\n"
-      "\t\tthreads (one per line) containing the matched messages.\n"
+      "Search for messages matching the given search terms.",
+      "\tNote that the individual mail messages will be matched\n"
+      "\tagainst the search terms, but the results will be the\n"
+      "\tthreads (one per line) containing the matched messages.\n"
       "\n"
-      "\t\tSupported options for search include:\n"
+      "\tSupported options for search include:\n"
       "\n"
-      "\t\t--sort=(newest-first|oldest-first)\n"
+      "\t--format=(json|text)\n"
       "\n"
-      "\t\t\tPresent results in either chronological order\n"
-      "\t\t\t(oldest-first) or reverse chronological order\n"
-      "\t\t\t(newest-first), which is the default.\n"
+      "\t\tPresents the results in either JSON or\n"
+      "\t\tplain-text (default)\n"
       "\n"
-      "\t\tSee \"notmuch help search-terms\" for details of the search\n"
-      "\t\tterms syntax." },
+      "\t--sort=(newest-first|oldest-first)\n"
+      "\n"
+      "\t\tPresent results in either chronological order\n"
+      "\t\t(oldest-first) or reverse chronological order\n"
+      "\t\t(newest-first), which is the default.\n"
+      "\n"
+      "\tSee \"notmuch help search-terms\" for details of the search\n"
+      "\tterms syntax." },
     { "show", notmuch_show_command,
       "<search-terms> [...]",
-      "\t\tShow all messages matching the search terms.",
-      "\t\tThe messages are grouped and sorted based on the threading\n"
-      "\t\t(all replies to a particular message appear immediately\n"
-      "\t\tafter that message in date order).\n"
-      "\n"
-      "\t\tSupported options for show include:\n"
-      "\n"
-      "\t\t--entire-thread\n"
-      "\n"
-      "\t\t\tBy default only those messages that match the\n"
-      "\t\t\tsearch terms will be displayed. With this option,\n"
-      "\t\t\tall messages in the same thread as any matched\n"
-      "\t\t\tmessage will be displayed.\n"
-      "\n"
-      "\t\tThe output format is plain-text, with all text-content\n"
-      "\t\tMIME parts decoded. Various components in the output,\n"
-      "\t\t('message', 'header', 'body', 'attachment', and MIME 'part')\n"
-      "\t\tare delimited by easily-parsed markers. Each marker consists\n"
-      "\t\tof a Control-L character (ASCII decimal 12), the name of\n"
-      "\t\tthe marker, and then either an opening or closing brace,\n"
-      "\t\t'{' or '}' to either open or close the component.\n"
-      "\n"
-      "\t\tA common use of \"notmuch show\" is to display a single\n"
-      "\t\tthread of email messages. For this, use a search term of\n"
-      "\t\t\"thread:<thread-id>\" as can be seen in the first column\n"
-      "\t\tof output from the \"notmuch search\" command.\n"
-      "\n"
-      "\t\tSee \"notmuch help search-terms\" for details of the search\n"
-      "\t\tterms syntax." },
+      "Show all messages matching the search terms.",
+      "\tThe messages are grouped and sorted based on the threading\n"
+      "\t(all replies to a particular message appear immediately\n"
+      "\tafter that message in date order).\n"
+      "\n"
+      "\tSupported options for show include:\n"
+      "\n"
+      "\t--entire-thread\n"
+      "\n"
+      "\t\tBy default only those messages that match the\n"
+      "\t\tsearch terms will be displayed. With this option,\n"
+      "\t\tall messages in the same thread as any matched\n"
+      "\t\tmessage will be displayed.\n"
+      "\n"
+      "\t--format=(json|text)\n"
+      "\n"
+      "\t\ttext\t(default)\n"
+      "\n"
+      "\t\tThe plain-text has all text-content MIME parts decoded.\n"
+      "\t\tVarious components in the output, ('message', 'header',\n"
+      "\t\t'body', 'attachment', and MIME 'part') are delimited by\n"
+      "\t\teasily-parsed markers. Each marker consists of a Control-L\n"
+      "\t\tcharacter (ASCII decimal 12), the name of the marker, and\n"
+      "\t\tthen either an opening or closing brace, '{' or '}' to\n"
+      "\t\teither open or close the component.\n"
+      "\n"
+      "\t\tjson\n"
+      "\n"
+      "\t\tFormat output as Javascript Object Notation (JSON).\n"
+      "\t\tJSON output always includes all messages in a matching,\n"
+      "\t\tthread i.e. '--output=json' implies '--entire-thread'\n"
+      "\n"
+      "\tA common use of \"notmuch show\" is to display a single\n"
+      "\tthread of email messages. For this, use a search term of\n"
+      "\t\"thread:<thread-id>\" as can be seen in the first column\n"
+      "\tof output from the \"notmuch search\" command.\n"
+      "\n"
+      "\tSee \"notmuch help search-terms\" for details of the search\n"
+      "\tterms syntax." },
     { "count", notmuch_count_command,
       "<search-terms> [...]",
-      "\t\tCount messages matching the search terms.",
-      "\t\tThe number of matching messages is output to stdout.\n"
+      "Count messages matching the search terms.",
+      "\tThe number of matching messages is output to stdout.\n"
       "\n"
-      "\t\tA common use of \"notmuch count\" is to display the count\n"
-      "\t\tof messages matching both a specific tag and either inbox\n"
-      "\t\tor unread\n"
+      "\tA common use of \"notmuch count\" is to display the count\n"
+      "\tof messages matching both a specific tag and either inbox\n"
+      "\tor unread\n"
       "\n"
-      "\t\tSee \"notmuch help search-terms\" for details of the search\n"
+      "\tSee \"notmuch help search-terms\" for details of the search\n"
       "\t\tterms syntax." },
     { "reply", notmuch_reply_command,
       "[options...] <search-terms> [...]",
-      "\t\tConstruct a reply template for a set of messages.",
-      "\t\tConstructs a new message as a reply to a set of existing\n"
-      "\t\tmessages. The Reply-To: header (if any, otherwise From:) is\n"
-      "\t\tused for the To: address. The To: and Cc: headers are copied,\n"
-      "\t\tbut not including any of the user's configured addresses.\n"
+      "Construct a reply template for a set of messages.",
+      "\tConstructs a new message as a reply to a set of existing\n"
+      "\tmessages. The Reply-To: header (if any, otherwise From:) is\n"
+      "\tused for the To: address. The To: and Cc: headers are copied,\n"
+      "\tbut not including any of the user's configured addresses.\n"
       "\n"
-      "\t\tA suitable subject is constructed. The In-Reply-to: and\n"
-      "\t\tReferences: headers are set appropriately, and the content\n"
-      "\t\tof the original messages is quoted and included in the body\n"
-      "\t\t(unless --format=headers-only is given).\n"
+      "\tA suitable subject is constructed. The In-Reply-to: and\n"
+      "\tReferences: headers are set appropriately, and the content\n"
+      "\tof the original messages is quoted and included in the body\n"
+      "\t(unless --format=headers-only is given).\n"
       "\n"
-      "\t\tThe resulting message template is output to stdout.\n"
+      "\tThe resulting message template is output to stdout.\n"
       "\n"
-      "\t\tSupported options for reply include:\n"
+      "\tSupported options for reply include:\n"
       "\n"
-      "\t\t--format=(default|headers-only)\n"
+      "\t--format=(default|headers-only)\n"
       "\n"
-      "\t\t\tdefault:\n"
-      "\t\t\t\tIncludes subject and quoted message body.\n"
+      "\t\tdefault:\n"
+      "\t\t\tIncludes subject and quoted message body.\n"
       "\n"
-      "\t\t\theaders-only:\n"
-      "\t\t\t\tOnly produces In-Reply-To, References, To\n"
-      "\t\t\t\tCc, and Bcc headers.\n"
+      "\t\theaders-only:\n"
+      "\t\t\tOnly produces In-Reply-To, References, To\n"
+      "\t\t\tCc, and Bcc headers.\n"
       "\n"
-      "\t\tSee \"notmuch help search-terms\" for details of the search\n"
-      "\t\tterms syntax." },
+      "\tSee \"notmuch help search-terms\" for details of the search\n"
+      "\tterms syntax." },
     { "tag", notmuch_tag_command,
       "+<tag>|-<tag> [...] [--] <search-terms> [...]",
-      "\t\tAdd/remove tags for all messages matching the search terms.",
-      "\t\tThe search terms are handled exactly as in 'search' so one\n"
-      "\t\tcan use that command first to see what will be modified.\n"
+      "Add/remove tags for all messages matching the search terms.",
+      "\tThe search terms are handled exactly as in 'search' so one\n"
+      "\tcan use that command first to see what will be modified.\n"
       "\n"
-      "\t\tTags prefixed by '+' are added while those prefixed by\n"
-      "\t\t'-' are removed. For each message, tag removal is performed\n"
-      "\t\tbefore tag addition.\n"
+      "\tTags prefixed by '+' are added while those prefixed by\n"
+      "\t'-' are removed. For each message, tag removal is performed\n"
+      "\tbefore tag addition.\n"
       "\n"
-      "\t\tThe beginning of <search-terms> is recognized by the first\n"
-      "\t\targument that begins with neither '+' nor '-'. Support for\n"
-      "\t\tan initial search term beginning with '+' or '-' is provided\n"
-      "\t\tby allowing the user to specify a \"--\" argument to separate\n"
-      "\t\tthe tags from the search terms.\n"
+      "\tThe beginning of <search-terms> is recognized by the first\n"
+      "\targument that begins with neither '+' nor '-'. Support for\n"
+      "\tan initial search term beginning with '+' or '-' is provided\n"
+      "\tby allowing the user to specify a \"--\" argument to separate\n"
+      "\tthe tags from the search terms.\n"
       "\n"
-      "\t\tSee \"notmuch help search-terms\" for details of the search\n"
-      "\t\tterms syntax." },
+      "\tSee \"notmuch help search-terms\" for details of the search\n"
+      "\tterms syntax." },
     { "dump", notmuch_dump_command,
       "[<filename>]",
-      "\t\tCreate a plain-text dump of the tags for each message.",
-      "\t\tOutput is to the given filename, if any, or to stdout.\n"
-      "\t\tThese tags are the only data in the notmuch database\n"
-      "\t\tthat can't be recreated from the messages themselves.\n"
-      "\t\tThe output of notmuch dump is therefore the only\n"
-      "\t\tcritical thing to backup (and much more friendly to\n"
-      "\t\tincremental backup than the native database files.)" },
+      "Create a plain-text dump of the tags for each message.",
+      "\tOutput is to the given filename, if any, or to stdout.\n"
+      "\tThese tags are the only data in the notmuch database\n"
+      "\tthat can't be recreated from the messages themselves.\n"
+      "\tThe output of notmuch dump is therefore the only\n"
+      "\tcritical thing to backup (and much more friendly to\n"
+      "\tincremental backup than the native database files.)" },
     { "restore", notmuch_restore_command,
       "<filename>",
-      "\t\tRestore the tags from the given dump file (see 'dump').",
-      "\t\tNote: The dump file format is specifically chosen to be\n"
-      "\t\tcompatible with the format of files produced by sup-dump.\n"
-      "\t\tSo if you've previously been using sup for mail, then the\n"
-      "\t\t\"notmuch restore\" command provides you a way to import\n"
-      "\t\tall of your tags (or labels as sup calls them)." },
+      "Restore the tags from the given dump file (see 'dump').",
+      "\tNote: The dump file format is specifically chosen to be\n"
+      "\tcompatible with the format of files produced by sup-dump.\n"
+      "\tSo if you've previously been using sup for mail, then the\n"
+      "\t\"notmuch restore\" command provides you a way to import\n"
+      "\tall of your tags (or labels as sup calls them)." },
     { "search-tags", notmuch_search_tags_command,
       "[<search-terms> [...] ]",
-      "\t\tList all tags found in the database or matching messages.",
-      "\t\tRun this command without any search-term(s) to obtain a list\n"
-      "\t\tof all tags found in the database. If you provide one or more\n"
-      "\t\tsearch-terms as argument(s) then the resulting list will\n"
-      "\t\tcontain tags only from messages that match the search-term(s).\n"
+      "List all tags found in the database or matching messages.",
+      "\tRun this command without any search-term(s) to obtain a list\n"
+      "\tof all tags found in the database. If you provide one or more\n"
+      "\tsearch-terms as argument(s) then the resulting list will\n"
+      "\tcontain tags only from messages that match the search-term(s).\n"
+      "\n"
+      "\tIn both cases the list will be alphabetically sorted." },
+    { "part", notmuch_part_command,
+      "--part=<num> <search-terms>",
+      "Output a single MIME part of a message.",
+      "\tA single decoded MIME part, with no encoding or framing,\n"
+      "\tis output to stdout. The search terms must match only a single\n"
+      "\tmessage, otherwise this command will fail.\n"
       "\n"
-      "\t\tIn both cases the list will be alphabetically sorted." },
+      "\tThe part number should match the part \"id\" field output\n"
+      "\tby the \"--format=json\" option of \"notmuch show\". If the\n"
+      "\tmessage specified by the search terms does not include a\n"
+      "\tpart with the specified \"id\" there will be no output." },
     { "help", notmuch_help_command,
       "[<command>]",
-      "\t\tThis message, or more detailed help for the named command.",
-      "\t\tExcept in this case, where there's not much more detailed\n"
-      "\t\thelp available." }
+      "This message, or more detailed help for the named command.",
+      "\tExcept in this case, where there's not much more detailed\n"
+      "\thelp available." }
 };
 
 static void
@@ -292,25 +318,25 @@ usage (FILE *out)
     command_t *command;
     unsigned int i;
 
-    fprintf (out, "Usage: notmuch <command> [args...]\n");
+    fprintf (out,
+            "Usage: notmuch --help\n"
+            "       notmuch --version\n"
+            "       notmuch <command> [args...]\n");
     fprintf (out, "\n");
-    fprintf (out, "Where <command> and [args...] are as follows:\n");
+    fprintf (out, "The available commands are as follows:\n");
     fprintf (out, "\n");
 
     for (i = 0; i < ARRAY_SIZE (commands); i++) {
        command = &commands[i];
 
-       if (command->arguments)
-           fprintf (out, "\t%s\t%s\n\n%s\n\n",
-                    command->name, command->arguments, command->summary);
-       else
-           fprintf (out, "\t%s\t%s\n\n",
-                    command->name, command->summary);
+       fprintf (out, "  %-11s  %s\n",
+                command->name, command->summary);
     }
 
+    fprintf (out, "\n");
     fprintf (out,
-    "Use \"notmuch help <command>\" for more details on each command.\n"
-    "And \"notmuch help search-terms\" for the common search-terms syntax.\n\n");
+    "Use \"notmuch help <command>\" for more details on each command\n"
+    "and \"notmuch help search-terms\" for the common search-terms syntax.\n\n");
 }
 
 static int
@@ -331,11 +357,11 @@ notmuch_help_command (unused (void *ctx), int argc, char *argv[])
        if (strcmp (argv[0], command->name) == 0) {
            printf ("Help for \"notmuch %s\":\n\n", argv[0]);
            if (command->arguments)
-               printf ("\t%s\t%s\n\n%s\n\n%s\n\n",
+               printf ("%s %s\n\n\t%s\n\n%s\n\n",
                        command->name, command->arguments,
                        command->summary, command->documentation);
            else
-               printf ("\t%s\t%s\n\n%s\n\n", command->name,
+               printf ("%s\t%s\n\n%s\n\n", command->name,
                        command->summary, command->documentation);
            return 0;
        }
@@ -442,6 +468,14 @@ main (int argc, char *argv[])
     if (argc == 1)
        return notmuch (local);
 
+    if (STRNCMP_LITERAL (argv[1], "--help") == 0)
+       return notmuch_help_command (NULL, 0, NULL);
+
+    if (STRNCMP_LITERAL (argv[1], "--version") == 0) {
+       printf ("notmuch version " STRINGIFY(NOTMUCH_VERSION) "\n");
+       return 0;
+    }
+
     for (i = 0; i < ARRAY_SIZE (commands); i++) {
        command = &commands[i];
 
index 819cd1edb4b4b715ac3bdf35a437a4a3f26deba2..f1600473bf60cdb21c4bcb05748ac8937590a1fc 100644 (file)
@@ -1,5 +1,5 @@
 [Desktop Entry]
-Name=Not Much Mail
+Name=Notmuch (emacs interface)
 Exec=emacs -f notmuch
 Icon=emblem-mail
 Terminal=false
diff --git a/notmuch.el b/notmuch.el
deleted file mode 100644 (file)
index 97914f2..0000000
+++ /dev/null
@@ -1,1496 +0,0 @@
-; notmuch.el --- run notmuch within emacs
-;
-; Copyright © Carl Worth
-;
-; 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: Carl Worth <cworth@cworth.org>
-
-; This is an emacs-based interface to the notmuch mail system.
-;
-; You will first need to have the notmuch program installed and have a
-; notmuch database built in order to use this. See
-; http://notmuchmail.org for details.
-;
-; To install this software, copy it to a directory that is on the
-; `load-path' variable within emacs (a good candidate is
-; /usr/local/share/emacs/site-lisp). If you are viewing this from the
-; notmuch source distribution then you can simply run:
-;
-;      sudo make install-emacs
-;
-; to install it.
-;
-; Then, to actually run it, add:
-;
-;      (require 'notmuch)
-;
-; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
-; or run:
-;
-;      emacs -f notmuch
-;
-; Have fun, and let us know if you have any comment, questions, or
-; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
-; required, but is available from http://notmuchmail.org).
-
-(require 'cl)
-(require 'mm-view)
-(require 'message)
-
-(defvar notmuch-show-mode-map
-  (let ((map (make-sparse-keymap)))
-    (define-key map "?" 'notmuch-help)
-    (define-key map "q" 'kill-this-buffer)
-    (define-key map (kbd "C-p") 'notmuch-show-previous-line)
-    (define-key map (kbd "C-n") 'notmuch-show-next-line)
-    (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
-    (define-key map (kbd "TAB") 'notmuch-show-next-button)
-    (define-key map "s" 'notmuch-search)
-    (define-key map "m" 'message-mail)
-    (define-key map "f" 'notmuch-show-forward-current)
-    (define-key map "r" 'notmuch-show-reply)
-    (define-key map "|" 'notmuch-show-pipe-message)
-    (define-key map "w" 'notmuch-show-save-attachments)
-    (define-key map "V" 'notmuch-show-view-raw-message)
-    (define-key map "v" 'notmuch-show-view-all-mime-parts)
-    (define-key map "-" 'notmuch-show-remove-tag)
-    (define-key map "+" 'notmuch-show-add-tag)
-    (define-key map "X" 'notmuch-show-mark-read-then-archive-then-exit)
-    (define-key map "x" 'notmuch-show-archive-thread-then-exit)
-    (define-key map "A" 'notmuch-show-mark-read-then-archive-thread)
-    (define-key map "a" 'notmuch-show-archive-thread)
-    (define-key map "p" 'notmuch-show-previous-message)
-    (define-key map "N" 'notmuch-show-mark-read-then-next-open-message)
-    (define-key map "n" 'notmuch-show-next-message)
-    (define-key map (kbd "DEL") 'notmuch-show-rewind)
-    (define-key map " " 'notmuch-show-advance-marking-read-and-archiving)
-    map)
-  "Keymap for \"notmuch show\" buffers.")
-(fset 'notmuch-show-mode-map notmuch-show-mode-map)
-
-(defvar notmuch-show-signature-regexp "\\(-- ?\\|_+\\)$"
-  "Pattern to match a line that separates content from signature.
-
-The regexp can (and should) include $ to match the end of the
-line, but should not include ^ to match the beginning of the
-line. This is because notmuch may have inserted additional space
-for indentation at the beginning of the line. But notmuch will
-move past the indentation when testing this pattern, (so that the
-pattern can still test against the entire line).")
-
-(defvar notmuch-show-signature-lines-max 12
-  "Maximum length of signature that will be hidden by default.")
-
-(defvar notmuch-command "notmuch"
-  "Command to run the notmuch binary.")
-
-(defvar notmuch-show-message-begin-regexp    "\fmessage{")
-(defvar notmuch-show-message-end-regexp      "\fmessage}")
-(defvar notmuch-show-header-begin-regexp     "\fheader{")
-(defvar notmuch-show-header-end-regexp       "\fheader}")
-(defvar notmuch-show-body-begin-regexp       "\fbody{")
-(defvar notmuch-show-body-end-regexp         "\fbody}")
-(defvar notmuch-show-attachment-begin-regexp "\fattachment{")
-(defvar notmuch-show-attachment-end-regexp   "\fattachment}")
-(defvar notmuch-show-part-begin-regexp       "\fpart{")
-(defvar notmuch-show-part-end-regexp         "\fpart}")
-(defvar notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$")
-
-(defvar notmuch-show-id-regexp "\\(id:[^ ]*\\)")
-(defvar notmuch-show-depth-match-regexp " depth:\\([0-9]*\\).*match:\\([01]\\) ")
-(defvar notmuch-show-filename-regexp "filename:\\(.*\\)$")
-(defvar notmuch-show-tags-regexp "(\\([^)]*\\))$")
-
-(defvar notmuch-show-parent-buffer nil)
-(defvar notmuch-show-body-read-visible nil)
-(defvar notmuch-show-citations-visible nil)
-(defvar notmuch-show-signatures-visible nil)
-(defvar notmuch-show-headers-visible nil)
-
-; XXX: This should be a generic function in emacs somewhere, not here
-(defun point-invisible-p ()
-  "Return whether the character at point is invisible.
-
-Here visibility is determined by `buffer-invisibility-spec' and
-the invisible property of any overlays for point. It doesn't have
-anything to do with whether point is currently being displayed
-within the current window."
-  (let ((prop (get-char-property (point) 'invisible)))
-    (if (eq buffer-invisibility-spec t)
-       prop
-      (or (memq prop buffer-invisibility-spec)
-         (assq prop buffer-invisibility-spec)))))
-
-(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
-  (let ((tag-list
-        (with-output-to-string
-          (with-current-buffer standard-output
-            (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
-    (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
-
-(defun notmuch-show-next-line ()
-  "Like builtin `next-line' but ensuring we end on a visible character.
-
-By advancing forward until reaching a visible character.
-
-Unlike builtin `next-line' this version accepts no arguments."
-  (interactive)
-  (set 'this-command 'next-line)
-  (call-interactively 'next-line)
-  (while (point-invisible-p)
-    (forward-char)))
-
-(defun notmuch-show-previous-line ()
-  "Like builtin `previous-line' but ensuring we end on a visible character.
-
-By advancing forward until reaching a visible character.
-
-Unlike builtin `previous-line' this version accepts no arguments."
-  (interactive)
-  (set 'this-command 'previous-line)
-  (call-interactively 'previous-line)
-  (while (point-invisible-p)
-    (forward-char)))
-
-(defun notmuch-show-get-message-id ()
-  (save-excursion
-    (beginning-of-line)
-    (if (not (looking-at notmuch-show-message-begin-regexp))
-       (re-search-backward notmuch-show-message-begin-regexp))
-    (re-search-forward notmuch-show-id-regexp)
-    (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
-
-(defun notmuch-show-get-filename ()
-  (save-excursion
-    (beginning-of-line)
-    (if (not (looking-at notmuch-show-message-begin-regexp))
-       (re-search-backward notmuch-show-message-begin-regexp))
-    (re-search-forward notmuch-show-filename-regexp)
-    (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
-
-(defun notmuch-show-set-tags (tags)
-  (save-excursion
-    (beginning-of-line)
-    (if (not (looking-at notmuch-show-message-begin-regexp))
-       (re-search-backward notmuch-show-message-begin-regexp))
-    (re-search-forward notmuch-show-tags-regexp)
-    (let ((inhibit-read-only t)
-         (beg (match-beginning 1))
-         (end (match-end 1)))
-      (delete-region beg end)
-      (goto-char beg)
-      (insert (mapconcat 'identity tags " ")))))
-
-(defun notmuch-show-get-tags ()
-  (save-excursion
-    (beginning-of-line)
-    (if (not (looking-at notmuch-show-message-begin-regexp))
-       (re-search-backward notmuch-show-message-begin-regexp))
-    (re-search-forward notmuch-show-tags-regexp)
-    (split-string (buffer-substring (match-beginning 1) (match-end 1)))))
-
-(defun notmuch-show-add-tag (&rest toadd)
-  "Add a tag to the current message."
-  (interactive
-   (list (notmuch-select-tag-with-completion "Tag to add: ")))
-  (apply 'notmuch-call-notmuch-process
-        (append (cons "tag"
-                      (mapcar (lambda (s) (concat "+" s)) toadd))
-                (cons (notmuch-show-get-message-id) nil)))
-  (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
-
-(defun notmuch-show-remove-tag (&rest toremove)
-  "Remove a tag from the current message."
-  (interactive
-   (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-show-get-message-id))))
-  (let ((tags (notmuch-show-get-tags)))
-    (if (intersection tags toremove :test 'string=)
-       (progn
-         (apply 'notmuch-call-notmuch-process
-                (append (cons "tag"
-                              (mapcar (lambda (s) (concat "-" s)) toremove))
-                        (cons (notmuch-show-get-message-id) nil)))
-         (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
-
-(defun notmuch-show-archive-thread-maybe-mark-read (markread)
-  (save-excursion
-    (goto-char (point-min))
-    (while (not (eobp))
-      (if markread
-         (notmuch-show-remove-tag "unread" "inbox")
-       (notmuch-show-remove-tag "inbox"))
-      (if (not (eobp))
-         (forward-char))
-      (if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
-         (goto-char (point-max)))))
-  (let ((parent-buffer notmuch-show-parent-buffer))
-    (kill-this-buffer)
-    (if parent-buffer
-       (progn
-         (switch-to-buffer parent-buffer)
-         (forward-line)
-         (notmuch-search-show-thread)))))
-
-(defun notmuch-show-mark-read-then-archive-thread ()
-  "Remove unread tags from thread, then archive and show next thread.
-
-Archive each message currently shown by removing the \"unread\"
-and \"inbox\" tag from each. Then kill this buffer and show the
-next thread from the search from which this thread was originally
-shown.
-
-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)
-  (notmuch-show-archive-thread-maybe-mark-read t))
-
-(defun notmuch-show-archive-thread ()
-  "Archive each message in thread, then show next thread from search.
-
-Archive each message currently shown by removing the \"inbox\"
-tag from each. Then kill this buffer and show the next thread
-from the search from which this thread was originally shown.
-
-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)
-  (notmuch-show-archive-thread-maybe-mark-read nil))
-
-(defun notmuch-show-archive-thread-then-exit ()
-  "Archive each message in thread, then exit back to search results."
-  (interactive)
-  (notmuch-show-archive-thread)
-  (kill-this-buffer))
-
-(defun notmuch-show-mark-read-then-archive-then-exit ()
-  "Remove unread tags from thread, then archive and exit to search results."
-  (interactive)
-  (notmuch-show-mark-read-then-archive-thread)
-  (kill-this-buffer))
-
-(defun notmuch-show-view-raw-message ()
-  "View the raw email of the current message."
-  (interactive)
-  (view-file (notmuch-show-get-filename)))
-
-(defmacro with-current-notmuch-show-message (&rest body)
-  "Evaluate body with current buffer set to the text of current message"
-  `(save-excursion
-     (let ((filename (notmuch-show-get-filename)))
-       (let ((buf (generate-new-buffer (concat "*notmuch-msg-" filename "*"))))
-         (with-current-buffer buf
-           (insert-file-contents filename nil nil nil t)
-           ,@body)
-        (kill-buffer buf)))))
-
-(defun notmuch-show-view-all-mime-parts ()
-  "Use external viewers to view all attachments from the current message."
-  (interactive)
-  (with-current-notmuch-show-message
-   (mm-display-parts (mm-dissect-buffer))))
-
-(defun notmuch-foreach-mime-part (function mm-handle)
-  (cond ((stringp (car mm-handle))
-         (dolist (part (cdr mm-handle))
-           (notmuch-foreach-mime-part function part)))
-        ((bufferp (car mm-handle))
-         (funcall function mm-handle))
-        (t (dolist (part mm-handle)
-             (notmuch-foreach-mime-part function part)))))
-
-(defun notmuch-count-attachments (mm-handle)
-  (let ((count 0))
-    (notmuch-foreach-mime-part
-     (lambda (p)
-       (let ((disposition (mm-handle-disposition p)))
-         (and (listp disposition)
-              (or (equal (car disposition) "attachment")
-                  (and (equal (car disposition) "inline")
-                       (assq 'filename disposition)))
-              (incf count))))
-     mm-handle)
-    count))
-
-(defun notmuch-save-attachments (mm-handle &optional queryp)
-  (notmuch-foreach-mime-part
-   (lambda (p)
-     (let ((disposition (mm-handle-disposition p)))
-       (and (listp disposition)
-            (or (equal (car disposition) "attachment")
-                (and (equal (car disposition) "inline")
-                     (assq 'filename disposition)))
-            (or (not queryp)
-                (y-or-n-p
-                 (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
-            (mm-save-part p))))
-   mm-handle))
-
-(defun notmuch-show-save-attachments ()
-  "Save all attachments from the current message."
-  (interactive)
-  (with-current-notmuch-show-message
-   (let ((mm-handle (mm-dissect-buffer)))
-     (notmuch-save-attachments
-      mm-handle (> (notmuch-count-attachments mm-handle) 1))))
-  (message "Done"))
-
-(defun notmuch-reply (query-string)
-  (switch-to-buffer (generate-new-buffer "notmuch-draft"))
-  (call-process notmuch-command nil t nil "reply" query-string)
-  (message-insert-signature)
-  (goto-char (point-min))
-  (if (re-search-forward "^$" nil t)
-      (progn
-       (insert "--text follows this line--")
-       (forward-line)))
-  (message-mode))
-
-(defun notmuch-show-reply ()
-  "Begin composing a reply to the current message in a new buffer."
-  (interactive)
-  (let ((message-id (notmuch-show-get-message-id)))
-    (notmuch-reply message-id)))
-
-(defun notmuch-show-forward-current ()
-  "Forward the current message."
-  (interactive)
-  (with-current-notmuch-show-message
-   (message-forward)))
-
-(defun notmuch-show-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 *Messages* buffer."
-  (interactive "sPipe message to command: ")
-  (apply 'start-process-shell-command "notmuch-pipe-command" "*notmuch-pipe*"
-        (list command " < " (shell-quote-argument (notmuch-show-get-filename)))))
-
-(defun notmuch-show-move-to-current-message-summary-line ()
-  "Move to the beginning of the one-line summary of the current message.
-
-This gives us a stable place to move to and work from since the
-summary line is always visible. This is important since moving to
-an invisible location is unreliable, (the main command loop moves
-point either forward or backward to the next visible character
-when a command ends with point on an invisible character).
-
-Emits an error if point is not within a valid message, (that is
-no pattern of `notmuch-show-message-begin-regexp' could be found
-by searching backward)."
-  (beginning-of-line)
-  (if (not (looking-at notmuch-show-message-begin-regexp))
-      (if (re-search-backward notmuch-show-message-begin-regexp nil t)
-         (forward-line 2)
-       (error "Not within a valid message."))
-    (forward-line 2)))
-
-(defun notmuch-show-last-message-p ()
-  "Predicate testing whether point is within the last message."
-  (save-window-excursion
-    (save-excursion
-      (notmuch-show-move-to-current-message-summary-line)
-      (not (re-search-forward notmuch-show-message-begin-regexp nil t)))))
-
-(defun notmuch-show-message-unread-p ()
-  "Predicate testing whether current message is unread."
-  (member "unread" (notmuch-show-get-tags)))
-
-(defun notmuch-show-message-open-p ()
-  "Predicate testing whether current message is open (body is visible)."
-  (let ((btn (previous-button (point) t)))
-    (while (not (button-has-type-p btn 'notmuch-button-body-toggle-type))
-      (setq btn (previous-button (button-start btn))))
-    (not (invisible-p (button-get btn 'invisibility-spec)))))
-
-(defun notmuch-show-next-message ()
-  "Advance to the beginning of the next message in the buffer.
-
-Moves to the last visible character of the current message if
-already on the last message in the buffer.
-
-Returns nil if already on the last message in the buffer."
-  (interactive)
-  (notmuch-show-move-to-current-message-summary-line)
-  (if (re-search-forward notmuch-show-message-begin-regexp nil t)
-      (progn
-       (notmuch-show-move-to-current-message-summary-line)
-       (recenter 0)
-       t)
-    (goto-char (- (point-max) 1))
-    (while (point-invisible-p)
-      (backward-char))
-    (recenter 0)
-    nil))
-
-(defun notmuch-show-find-next-message ()
-  "Returns the position of the next message in the buffer.
-
-Or the position of the last visible character of the current
-message if already within the last message in the buffer."
-  ; save-excursion doesn't save our window position
-  ; save-window-excursion doesn't save point
-  ; Looks like we have to use both.
-  (save-excursion
-    (save-window-excursion
-      (notmuch-show-next-message)
-      (point))))
-
-(defun notmuch-show-next-unread-message ()
-  "Advance to the beginning of the next unread message in the buffer.
-
-Moves to the last visible character of the current message if
-there are no more unread messages past the current point."
-  (notmuch-show-next-message)
-  (while (and (not (notmuch-show-last-message-p))
-             (not (notmuch-show-message-unread-p)))
-    (notmuch-show-next-message))
-  (if (not (notmuch-show-message-unread-p))
-      (notmuch-show-next-message)))
-
-(defun notmuch-show-next-open-message ()
-  "Advance to the next open message (that is, body is not invisible)."
-  (while (and (notmuch-show-next-message)
-             (not (notmuch-show-message-open-p)))))
-
-(defun notmuch-show-previous-message ()
-  "Backup to the beginning of the previous message in the buffer.
-
-If within a message rather than at the beginning of it, then
-simply move to the beginning of the current message."
-  (interactive)
-  (let ((start (point)))
-    (notmuch-show-move-to-current-message-summary-line)
-    (if (not (< (point) start))
-       ; Go backward twice to skip the current message's marker
-       (progn
-         (re-search-backward notmuch-show-message-begin-regexp nil t)
-         (re-search-backward notmuch-show-message-begin-regexp nil t)
-         (notmuch-show-move-to-current-message-summary-line)
-         ))
-    (recenter 0)))
-
-(defun notmuch-show-find-previous-message ()
-  "Returns the position of the previous message in the buffer.
-
-Or the position of the beginning of the current message if point
-is originally within the message rather than at the beginning of
-it."
-  ; save-excursion doesn't save our window position
-  ; save-window-excursion doesn't save point
-  ; Looks like we have to use both.
-  (save-excursion
-    (save-window-excursion
-      (notmuch-show-previous-message)
-      (point))))
-
-(defun notmuch-show-mark-read-then-next-open-message ()
-  "Remove unread tag from this message, then advance to next open message."
-  (interactive)
-  (notmuch-show-remove-tag "unread")
-  (notmuch-show-next-open-message))
-
-(defun notmuch-show-rewind ()
-  "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-marking-read-and-archiving]).
-
-Specifically, if the beginning of the previous email is fewer
-than `window-height' lines from the current point, move to it
-just like `notmuch-show-previous-message'.
-
-Otherwise, just scroll down a screenful of the current message.
-
-This command does not modify any message tags, (it does not undo
-any effects from previous calls to
-`notmuch-show-advance-marking-read-and-archiving'."
-  (interactive)
-  (let ((previous (notmuch-show-find-previous-message)))
-    (if (> (count-lines previous (point)) (- (window-height) next-screen-context-lines))
-       (progn
-         (condition-case nil
-             (scroll-down nil)
-           ((beginning-of-buffer) nil))
-         (goto-char (window-start)))
-      (notmuch-show-previous-message))))
-
-(defun notmuch-show-advance-marking-read-and-archiving ()
-  "Advance through thread, marking read and archiving.
-
-This command is intended to be one of the simplest ways to
-process a thread of email. It does the following:
-
-If the current message in the thread is not yet fully visible,
-scroll by a near screenful to read more of the message.
-
-Otherwise, (the end of the current message is already within the
-current window), remove the \"unread\" tag (if present) from the
-current message and advance to the next open message.
-
-Finally, if there is no further message to advance to, and this
-last message is already read, then archive the entire current
-thread, (remove the \"inbox\" tag from each message). Also kill
-this buffer, and display the next thread from the search from
-which this thread was originally shown."
-  (interactive)
-  (let ((next (notmuch-show-find-next-message))
-       (unread (notmuch-show-message-unread-p)))
-    (if (> next (window-end))
-       (scroll-up nil)
-      (let ((last (notmuch-show-last-message-p)))
-       (notmuch-show-mark-read-then-next-open-message)
-       (if last
-           (notmuch-show-archive-thread))))))
-
-(defun notmuch-show-next-button ()
-  "Advance point to the next button in the buffer."
-  (interactive)
-  (goto-char (button-start (next-button (point)))))
-
-(defun notmuch-show-previous-button ()
-  "Move point back to the previous button in the buffer."
-  (interactive)
-  (goto-char (button-start (previous-button (point)))))
-
-(defun notmuch-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)
-          ))
-  (force-window-update)
-  (redisplay t))
-
-(define-button-type 'notmuch-button-invisibility-toggle-type
-  'action 'notmuch-toggle-invisible-action
-  'follow-link t
-  'face "default")
-(define-button-type 'notmuch-button-citation-toggle-type 'help-echo "mouse-1, RET: Show citation"
-  :supertype 'notmuch-button-invisibility-toggle-type)
-(define-button-type 'notmuch-button-signature-toggle-type 'help-echo "mouse-1, RET: Show signature"
-  :supertype 'notmuch-button-invisibility-toggle-type)
-(define-button-type 'notmuch-button-headers-toggle-type 'help-echo "mouse-1, RET: Show headers"
-  :supertype 'notmuch-button-invisibility-toggle-type)
-(define-button-type 'notmuch-button-body-toggle-type
-  'help-echo "mouse-1, RET: Show message"
-  'face 'notmuch-message-summary-face
-  :supertype 'notmuch-button-invisibility-toggle-type)
-
-(defun notmuch-show-markup-citations-region (beg end depth)
-  (goto-char beg)
-  (beginning-of-line)
-  (while (< (point) end)
-    (let ((beg-sub (point-marker))
-         (indent (make-string depth ? ))
-         (citation ">"))
-      (move-to-column depth)
-      (if (looking-at citation)
-         (progn
-           (while (looking-at citation)
-             (forward-line)
-             (move-to-column depth))
-           (let ((overlay (make-overlay beg-sub (point)))
-                  (invis-spec (make-symbol "notmuch-citation-region")))
-              (add-to-invisibility-spec invis-spec)
-             (overlay-put overlay 'invisible invis-spec)
-              (let ((p (point-marker))
-                    (cite-button-text
-                     (concat "["  (number-to-string (count-lines beg-sub (point)))
-                             "-line citation. Click/Enter to show.]")))
-                (goto-char (- beg-sub 1))
-                (insert (concat "\n" indent))
-                (insert-button cite-button-text
-                               'invisibility-spec invis-spec
-                               :type 'notmuch-button-citation-toggle-type)
-                (forward-line)
-              ))))
-      (move-to-column depth)
-      (if (looking-at notmuch-show-signature-regexp)
-         (let ((sig-lines (- (count-lines beg-sub end) 1)))
-           (if (<= sig-lines notmuch-show-signature-lines-max)
-               (progn
-                  (let ((invis-spec (make-symbol "notmuch-signature-region")))
-                    (add-to-invisibility-spec invis-spec)
-                    (overlay-put (make-overlay beg-sub end)
-                                 'invisible invis-spec)
-                  
-                    (goto-char (- beg-sub 1))
-                    (insert (concat "\n" indent))
-                    (let ((sig-button-text (concat "[" (number-to-string sig-lines)
-                                                   "-line signature. Click/Enter to show.]")))
-                      (insert-button sig-button-text 'invisibility-spec invis-spec
-                                     :type 'notmuch-button-signature-toggle-type)
-                     )
-                    (insert "\n")
-                    (goto-char end))))))
-      (forward-line))))
-
-(defun notmuch-show-markup-part (beg end depth)
-  (if (re-search-forward notmuch-show-part-begin-regexp nil t)
-      (progn
-       (forward-line)
-       (let ((beg (point-marker)))
-         (re-search-forward notmuch-show-part-end-regexp)
-         (let ((end (copy-marker (match-beginning 0))))
-           (goto-char end)
-           (if (not (bolp))
-               (insert "\n"))
-           (indent-rigidly beg end depth)
-           (notmuch-show-markup-citations-region beg end depth)
-           ; Advance to the next part (if any) (so the outer loop can
-           ; determine whether we've left the current message.
-           (if (re-search-forward notmuch-show-part-begin-regexp nil t)
-               (beginning-of-line)))))
-    (goto-char end)))
-
-(defun notmuch-show-markup-parts-region (beg end depth)
-  (save-excursion
-    (goto-char beg)
-    (while (< (point) end)
-      (notmuch-show-markup-part beg end depth))))
-
-(defun notmuch-show-markup-body (depth match btn)
-  "Markup a message body, (indenting, buttonizing citations,
-etc.), and conditionally hiding the body itself if the message
-has been read and does not match the current search.
-
-DEPTH specifies the depth at which this message appears in the
-tree of the current thread, (the top-level messages have depth 0
-and each reply increases depth by 1). MATCH indicates whether
-this message is regarded as matching the current search. BTN is
-the button which is used to toggle the visibility of this
-message.
-
-When this function is called, point must be within the message, but
-before the delimiter marking the beginning of the body."
-  (re-search-forward notmuch-show-body-begin-regexp)
-  (forward-line)
-  (let ((beg (point-marker)))
-    (re-search-forward notmuch-show-body-end-regexp)
-    (let ((end (copy-marker (match-beginning 0))))
-      (notmuch-show-markup-parts-region beg end depth)
-      (let ((invis-spec (make-symbol "notmuch-show-body-read")))
-        (overlay-put (make-overlay beg end)
-                     'invisible invis-spec)
-        (button-put btn 'invisibility-spec invis-spec)
-        (if (not (or (notmuch-show-message-unread-p) match))
-            (add-to-invisibility-spec invis-spec)))
-      (set-marker beg nil)
-      (set-marker end nil)
-      )))
-
-(defun notmuch-fontify-headers ()
-  (while (looking-at "[[:space:]]")
-    (forward-char))
-  (if (looking-at "[Tt]o:")
-      (progn
-       (overlay-put (make-overlay (point) (re-search-forward ":"))
-                    'face 'message-header-name)
-       (overlay-put (make-overlay (point) (re-search-forward ".*$"))
-                    'face 'message-header-to))
-    (if (looking-at "[B]?[Cc][Cc]:")
-       (progn
-         (overlay-put (make-overlay (point) (re-search-forward ":"))
-                      'face 'message-header-name)
-         (overlay-put (make-overlay (point) (re-search-forward ".*$"))
-                      'face 'message-header-cc))
-      (if (looking-at "[Ss]ubject:")
-         (progn
-           (overlay-put (make-overlay (point) (re-search-forward ":"))
-                        'face 'message-header-name)
-           (overlay-put (make-overlay (point) (re-search-forward ".*$"))
-                        'face 'message-header-subject))
-       (if (looking-at "[Ff]rom:")
-           (progn
-             (overlay-put (make-overlay (point) (re-search-forward ":"))
-                          'face 'message-header-name)
-             (overlay-put (make-overlay (point) (re-search-forward ".*$"))
-                          'face 'message-header-other)))))))
-
-(defun notmuch-show-markup-header (message-begin depth)
-  "Buttonize and decorate faces in a message header.
-
-MESSAGE-BEGIN is the position of the absolute first character in
-the message (including all delimiters that will end up being
-invisible etc.). This is to allow a button to reliably extend to
-the beginning of the message even if point is positioned at an
-invisible character (such as the beginning of the buffer).
-
-DEPTH specifies the depth at which this message appears in the
-tree of the current thread, (the top-level messages have depth 0
-and each reply increases depth by 1)."
-  (re-search-forward notmuch-show-header-begin-regexp)
-  (forward-line)
-  (let ((beg (point-marker))
-       (summary-end (copy-marker (line-beginning-position 2)))
-       (subject-end (copy-marker (line-end-position 2)))
-       (invis-spec (make-symbol "notmuch-show-header"))
-        (btn nil))
-    (re-search-forward notmuch-show-header-end-regexp)
-    (beginning-of-line)
-    (let ((end (point-marker)))
-      (indent-rigidly beg end depth)
-      (goto-char beg)
-      (setq btn (make-button message-begin summary-end :type 'notmuch-button-body-toggle-type))
-      (forward-line)
-      (add-to-invisibility-spec invis-spec)
-      (overlay-put (make-overlay subject-end end)
-                  'invisible invis-spec)
-      (make-button (line-beginning-position) subject-end
-                  'invisibility-spec invis-spec
-                  :type 'notmuch-button-headers-toggle-type)
-      (while (looking-at "[[:space:]]*[A-Za-z][-A-Za-z0-9]*:")
-       (beginning-of-line)
-       (notmuch-fontify-headers)
-       (forward-line)
-       )
-      (goto-char end)
-      (insert "\n")
-      (set-marker beg nil)
-      (set-marker summary-end nil)
-      (set-marker subject-end nil)
-      (set-marker end nil)
-      )
-  btn))
-
-(defun notmuch-show-markup-message ()
-  (if (re-search-forward notmuch-show-message-begin-regexp nil t)
-      (let ((message-begin (match-beginning 0)))
-       (re-search-forward notmuch-show-depth-match-regexp)
-       (let ((depth (string-to-number (buffer-substring (match-beginning 1) (match-end 1))))
-             (match (string= "1" (buffer-substring (match-beginning 2) (match-end 2))))
-              (btn nil))
-         (setq btn (notmuch-show-markup-header message-begin depth))
-         (notmuch-show-markup-body depth match btn)))
-    (goto-char (point-max))))
-
-(defun notmuch-show-hide-markers ()
-  (save-excursion
-    (goto-char (point-min))
-    (while (not (eobp))
-      (if (re-search-forward notmuch-show-marker-regexp nil t)
-         (progn
-           (overlay-put (make-overlay (match-beginning 0) (+ (match-end 0) 1))
-                        'invisible 'notmuch-show-marker))
-       (goto-char (point-max))))))
-
-(defun notmuch-show-markup-messages ()
-  (save-excursion
-    (goto-char (point-min))
-    (while (not (eobp))
-      (notmuch-show-markup-message)))
-  (notmuch-show-hide-markers))
-
-(defun notmuch-documentation-first-line (symbol)
-  "Return the first line of the documentation string for SYMBOL."
-  (let ((doc (documentation symbol)))
-    (if doc
-       (with-temp-buffer
-         (insert (documentation symbol t))
-         (goto-char (point-min))
-         (let ((beg (point)))
-           (end-of-line)
-           (buffer-substring beg (point))))
-      "")))
-
-(defun notmuch-prefix-key-description (key)
-  "Given a prefix key code, return a human-readable string representation.
-
-This is basically just `format-kbd-macro' but we also convert ESC to M-."
-  (let ((desc (format-kbd-macro (vector key))))
-    (if (string= desc "ESC")
-       "M-"
-      (concat desc " "))))
-
-; I would think that emacs would have code handy for walking a keymap
-; and generating strings for each key, and I would prefer to just call
-; that. But I couldn't find any (could be all implemented in C I
-; suppose), so I wrote my own here.
-(defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
-  "For a key binding, return a string showing a human-readable
-representation of the prefixed key as well as the first line of
-documentation from the bound function.
-
-For a mouse binding, return nil."
-  (let ((key (car binding))
-       (action (cdr binding)))
-    (if (mouse-event-p key)
-       nil
-      (if (keymapp action)
-         (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key))))
-           (mapconcat substitute (cdr action) "\n"))
-       (concat prefix (format-kbd-macro (vector key))
-               "\t"
-               (notmuch-documentation-first-line action))))))
-
-(defalias 'notmuch-substitute-one-command-key
-  (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil))
-
-(defun notmuch-substitute-command-keys (doc)
-  "Like `substitute-command-keys' but with documentation, not function names."
-  (let ((beg 0))
-    (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
-      (let ((map (substring doc (match-beginning 1) (match-end 1))))
-       (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key
-                                           (cdr (symbol-value (intern map))) "\n") 1 1 doc)))
-      (setq beg (match-end 0)))
-    doc))
-
-(defun notmuch-help ()
-  "Display help for the current notmuch mode."
-  (interactive)
-  (let* ((mode major-mode)
-        (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
-    (with-current-buffer (generate-new-buffer "*notmuch-help*")
-      (insert doc)
-      (goto-char (point-min))
-      (set-buffer-modified-p nil)
-      (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
-
-;;;###autoload
-(defun notmuch-show-mode ()
-  "Major mode for viewing a thread with notmuch.
-
-This buffer contains the results of the \"notmuch show\" command
-for displaying a single thread of email from your email archives.
-
-By default, various components of email messages, (citations,
-signatures, already-read messages), are hidden. You can make
-these parts visible by clicking with the mouse button or by
-pressing RET after positioning the cursor on a hidden part, (for
-which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
-
-Reading the thread sequentially is well-supported by pressing
-\\[notmuch-show-advance-marking-read-and-archiving]. This will scroll the current message (if necessary),
-advance to the next message, or advance to the next thread (if
-already on the last message of a thread). As each message is
-scrolled away its \"unread\" tag will be removed, and as each
-thread is scrolled away the \"inbox\" tag will be removed from
-each message in the thread.
-
-Other commands are available to read or manipulate the thread more
-selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages without
-removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread without
-scrolling through with \\[notmuch-show-advance-marking-read-and-archiving]).
-
-You can add or remove arbitary tags from the current message with
-'\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
-
-All currently available key bindings:
-
-\\{notmuch-show-mode-map}"
-  (interactive)
-  (kill-all-local-variables)
-  (add-to-invisibility-spec 'notmuch-show-marker)
-  (use-local-map notmuch-show-mode-map)
-  (setq major-mode 'notmuch-show-mode
-       mode-name "notmuch-show")
-  (setq buffer-read-only t))
-
-(defgroup notmuch nil
-  "Notmuch mail reader for Emacs."
-  :group 'mail)
-
-(defcustom notmuch-show-hook nil
-  "List of functions to call when notmuch displays a message."
-  :type 'hook
-  :options '(goto-address)
-  :group 'notmuch)
-
-(defcustom notmuch-search-hook nil
-  "List of functions to call when notmuch displays the search results."
-  :type 'hook
-  :options '(hl-line-mode)
-  :group 'notmuch)
-
-; Make show mode a bit prettier, highlighting URLs and using word wrap
-
-(defun notmuch-show-pretty-hook ()
-  (goto-address-mode 1)
-  (visual-line-mode))
-
-(add-hook 'notmuch-show-hook 'notmuch-show-pretty-hook)
-(add-hook 'notmuch-search-hook
-         (lambda()
-           (hl-line-mode 1) ))
-
-(defun notmuch-show (thread-id &optional parent-buffer query-context)
-  "Run \"notmuch show\" with the given thread ID and display results.
-
-The optional PARENT-BUFFER is the notmuch-search buffer from
-which this notmuch-show command was executed, (so that the next
-thread from that buffer can be show when done with this one).
-
-The optional QUERY-CONTEXT is a notmuch search term. Only messages from the thread 
-matching this search term are shown if non-nil. "
-  (interactive "sNotmuch show: ")
-  (let ((buffer (get-buffer-create (concat "*notmuch-show-" thread-id "*"))))
-    (switch-to-buffer buffer)
-    (notmuch-show-mode)
-    (set (make-local-variable 'notmuch-show-parent-buffer) parent-buffer)
-    (let ((proc (get-buffer-process (current-buffer)))
-         (inhibit-read-only t))
-      (if proc
-         (error "notmuch search process already running for query `%s'" thread-id)
-       )
-      (erase-buffer)
-      (goto-char (point-min))
-      (save-excursion
-       (let* ((basic-args (list notmuch-command nil t nil "show" "--entire-thread" thread-id))
-               (args (if query-context (append basic-args (list "and (" query-context ")")) basic-args)))
-         (apply 'call-process args)
-         (when (and (eq (buffer-size) 0) query-context)
-           (apply 'call-process basic-args)))
-       (notmuch-show-markup-messages)
-       )
-      (run-hooks 'notmuch-show-hook)
-      ; Move straight to the first open message
-      (if (not (notmuch-show-message-open-p))
-         (notmuch-show-next-open-message))
-      )))
-
-(defvar notmuch-search-authors-width 40
-  "Number of columns to use to display authors in a notmuch-search buffer.")
-
-(defvar notmuch-search-mode-map
-  (let ((map (make-sparse-keymap)))
-    (define-key map "?" 'notmuch-help)
-    (define-key map "q" 'kill-this-buffer)
-    (define-key map "x" 'kill-this-buffer)
-    (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
-    (define-key map "b" 'notmuch-search-scroll-down)
-    (define-key map " " 'notmuch-search-scroll-up)
-    (define-key map "<" 'notmuch-search-first-thread)
-    (define-key map ">" 'notmuch-search-last-thread)
-    (define-key map "p" 'notmuch-search-previous-thread)
-    (define-key map "n" 'notmuch-search-next-thread)
-    (define-key map "r" 'notmuch-search-reply-to-thread)
-    (define-key map "m" 'message-mail)
-    (define-key map "s" 'notmuch-search)
-    (define-key map "o" 'notmuch-search-toggle-order)
-    (define-key map "=" 'notmuch-search-refresh-view)
-    (define-key map "t" 'notmuch-search-filter-by-tag)
-    (define-key map "f" 'notmuch-search-filter)
-    (define-key map [mouse-1] 'notmuch-search-show-thread)
-    (define-key map "*" 'notmuch-search-operate-all)
-    (define-key map "a" 'notmuch-search-archive-thread)
-    (define-key map "-" 'notmuch-search-remove-tag)
-    (define-key map "+" 'notmuch-search-add-tag)
-    (define-key map (kbd "RET") 'notmuch-search-show-thread)
-    map)
-  "Keymap for \"notmuch search\" buffers.")
-(fset 'notmuch-search-mode-map notmuch-search-mode-map)
-
-(defvar notmuch-search-query-string)
-(defvar notmuch-search-oldest-first t
-  "Show the oldest mail first in the search-mode")
-
-(defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
-
-(defun notmuch-search-scroll-up ()
-  "Move forward through search results by one window's worth."
-  (interactive)
-  (condition-case nil
-      (scroll-up nil)
-    ((end-of-buffer) (notmuch-search-last-thread))))
-
-(defun notmuch-search-scroll-down ()
-  "Move backward through the search results by one window's worth."
-  (interactive)
-  ; I don't know why scroll-down doesn't signal beginning-of-buffer
-  ; the way that scroll-up signals end-of-buffer, but c'est la vie.
-  ;
-  ; So instead of trapping a signal we instead check whether the
-  ; window begins on the first line of the buffer and if so, move
-  ; directly to that position. (We have to count lines since the
-  ; window-start position is not the same as point-min due to the
-  ; invisible thread-ID characters on the first line.
-  (if (equal (count-lines (point-min) (window-start)) 0)
-      (goto-char (point-min))
-    (scroll-down nil)))
-
-(defun notmuch-search-next-thread ()
-  "Select the next thread in the search results."
-  (interactive)
-  (forward-line 1))
-
-(defun notmuch-search-previous-thread ()
-  "Select the previous thread in the search results."
-  (interactive)
-  (forward-line -1))
-
-(defun notmuch-search-last-thread ()
-  "Select the last thread in the search results."
-  (interactive)
-  (goto-char (point-max))
-  (forward-line -2))
-
-(defun notmuch-search-first-thread ()
-  "Select the first thread in the search results."
-  (interactive)
-  (goto-char (point-min)))
-
-(defface notmuch-message-summary-face
- '((((class color) (background light)) (:background "#f0f0f0"))
-   (((class color) (background dark)) (:background "#303030")))
- "Face for the single-line message summary in notmuch-show-mode."
- :group 'notmuch)
-
-(defface notmuch-tag-face
-  '((((class color)
-      (background dark))
-     (:foreground "OliveDrab1"))
-    (((class color)
-      (background light))
-     (:foreground "navy blue" :bold t))
-    (t
-     (:bold t)))
-  "Notmuch search mode face used to highligh tags."
-  :group 'notmuch)
-
-(defvar notmuch-tag-face-alist nil
-  "List containing the tag list that need to be highlighed")
-
-(defvar notmuch-search-font-lock-keywords  nil)
-
-;;;###autoload
-(defun notmuch-search-mode ()
-  "Major mode displaying results of a notmuch search.
-
-This buffer contains the results of a \"notmuch search\" of your
-email archives. Each line in the buffer represents a single
-thread giving a summary of the thread (a relative date, the
-number of matched messages and total messages in the thread,
-participants in the thread, a representative subject line, and
-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-operate-all]' key can be used to add or remove a tag from all
-threads in the current 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
-only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
-search.
-
-Complete list of currently available key bindings:
-
-\\{notmuch-search-mode-map}"
-  (interactive)
-  (kill-all-local-variables)
-  (make-local-variable 'notmuch-search-query-string)
-  (make-local-variable 'notmuch-search-oldest-first)
-  (set (make-local-variable 'scroll-preserve-screen-position) t)
-  (add-to-invisibility-spec 'notmuch-search)
-  (use-local-map notmuch-search-mode-map)
-  (setq truncate-lines t)
-  (setq major-mode 'notmuch-search-mode
-       mode-name "notmuch-search")
-  (setq buffer-read-only t)
-  (if (not notmuch-tag-face-alist)
-      (add-to-list 'notmuch-search-font-lock-keywords (list
-               "(\\([^)]*\\))$" '(1  'notmuch-tag-face)))
-    (let ((notmuch-search-tags (mapcar 'car notmuch-tag-face-alist)))
-      (loop for notmuch-search-tag  in notmuch-search-tags
-           do (add-to-list 'notmuch-search-font-lock-keywords (list
-                       (concat "([^)]*\\(" notmuch-search-tag "\\)[^)]*)$")
-                       `(1  ,(cdr (assoc notmuch-search-tag notmuch-tag-face-alist))))))))
-  (set (make-local-variable 'font-lock-defaults)
-         '(notmuch-search-font-lock-keywords t)))
-
-(defun notmuch-search-find-thread-id ()
-  "Return the thread for the current thread"
-  (get-text-property (point) 'notmuch-search-thread-id))
-
-(defun notmuch-search-find-authors ()
-  "Return the authors for the current thread"
-  (get-text-property (point) 'notmuch-search-authors))
-
-(defun notmuch-search-find-subject ()
-  "Return the subject for the current thread"
-  (get-text-property (point) 'notmuch-search-subject))
-
-(defun notmuch-search-show-thread ()
-  "Display the currently selected thread."
-  (interactive)
-  (let ((thread-id (notmuch-search-find-thread-id)))
-    (if (> (length thread-id) 0)
-       (notmuch-show thread-id (current-buffer) notmuch-search-query-string)
-      (error "End of search results"))))
-
-(defun notmuch-search-reply-to-thread ()
-  "Begin composing a reply to the entire current thread in a new buffer."
-  (interactive)
-  (let ((message-id (notmuch-search-find-thread-id)))
-    (notmuch-reply message-id)))
-
-(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 (mapconcat  'identity tags " "))))))
-
-(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 beg end))))))
-
-(defun notmuch-search-add-tag (tag)
-  "Add a tag to the currently selected thread.
-
-The tag is added to messages in the currently selected thread
-which match the current search terms."
-  (interactive
-   (list (notmuch-select-tag-with-completion "Tag to add: ")))
-  (notmuch-call-notmuch-process "tag" (concat "+" tag) (notmuch-search-find-thread-id))
-  (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))))
-
-(defun notmuch-search-remove-tag (tag)
-  "Remove a tag from the currently selected thread.
-
-The tag is removed from messages in the currently selected thread
-which match the current search terms."
-  (interactive
-   (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-search-find-thread-id))))
-  (notmuch-call-notmuch-process "tag" (concat "-" tag) (notmuch-search-find-thread-id))
-  (notmuch-search-set-tags (delete tag (notmuch-search-get-tags))))
-
-(defun notmuch-search-archive-thread ()
-  "Archive the currently selected thread (remove its \"inbox\" tag).
-
-This function advances the next thread when finished."
-  (interactive)
-  (notmuch-search-remove-tag "inbox")
-  (forward-line))
-
-(defun notmuch-search-process-sentinel (proc msg)
-  "Add a message to let user know when \"notmuch search\" exits"
-  (let ((buffer (process-buffer proc))
-       (status (process-status proc))
-       (exit-status (process-exit-status proc)))
-    (if (memq status '(exit signal))
-       (if (buffer-live-p buffer)
-           (with-current-buffer buffer
-             (save-excursion
-               (let ((inhibit-read-only t))
-                 (goto-char (point-max))
-                 (if (eq status 'signal)
-                     (insert "Incomplete search results (search process was killed).\n"))
-                 (if (eq status 'exit)
-                     (progn
-                       (insert "End of search results.")
-                       (if (not (= exit-status 0))
-                           (insert (format " (process returned %d)" exit-status)))
-                       (insert "\n"))))))))))
-
-(defun notmuch-search-process-filter (proc string)
-  "Process and filter the output of \"notmuch search\""
-  (let ((buffer (process-buffer proc)))
-    (if (buffer-live-p buffer)
-       (with-current-buffer buffer
-         (save-excursion
-           (let ((line 0)
-                 (more t)
-                 (inhibit-read-only t))
-             (while more
-               (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))
-                          (authors-length (length authors))
-                          (subject (match-string 5 string))
-                          (tags (match-string 6 string)))
-                     (if (> authors-length 40)
-                         (set 'authors (concat (substring authors 0 (- 40 3)) "...")))
-                     (goto-char (point-max))
-                     (let ((beg (point-marker)))
-                       (insert (format "%s %-7s %-40s %s (%s)\n" date count authors subject tags))
-                       (put-text-property beg (point-marker) 'notmuch-search-thread-id thread-id)
-                       (put-text-property beg (point-marker) 'notmuch-search-authors authors)
-                       (put-text-property beg (point-marker) 'notmuch-search-subject subject))
-                     (set 'line (match-end 0)))
-                 (set 'more nil))))))
-      (delete-process proc))))
-
-(defun notmuch-search-operate-all (action)
-  "Add/remove tags from all matching messages.
-
-Tis command adds or removes tags from all messages matching the
-current search terms. When called interactively, this command
-will prompt for tags to be added or removed. Tags prefixed with
-'+' will be added and tags prefixed with '-' will be removed.
-
-Each character of the tag name may consist of alphanumeric
-characters as well as `_.+-'.
-"
-  (interactive "sOperation (+add -drop): notmuch tag ")
-  (let ((action-split (split-string action " +")))
-    ;; Perform some validation
-    (let ((words action-split))
-      (when (null words) (error "No operation given"))
-      (while words
-       (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
-         (error "Action must be of the form `+thistag -that_tag'"))
-       (setq words (cdr words))))
-    (apply 'notmuch-call-notmuch-process "tag"
-          (append action-split (list notmuch-search-query-string) nil))))
-
-;;;###autoload
-(defun notmuch-search (query &optional oldest-first)
-  "Run \"notmuch search\" with the given query string and display results."
-  (interactive "sNotmuch search: ")
-  (let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
-    (switch-to-buffer buffer)
-    (notmuch-search-mode)
-    (set 'notmuch-search-query-string query)
-    (set 'notmuch-search-oldest-first oldest-first)
-    (let ((proc (get-buffer-process (current-buffer)))
-         (inhibit-read-only t))
-      (if proc
-         (error "notmuch search process already running for query `%s'" query)
-       )
-      (erase-buffer)
-      (goto-char (point-min))
-      (save-excursion
-       (let ((proc (start-process-shell-command
-                    "notmuch-search" buffer notmuch-command "search"
-                    (if oldest-first "--sort=oldest-first" "--sort=newest-first")
-                    (shell-quote-argument query))))
-         (set-process-sentinel proc 'notmuch-search-process-sentinel)
-         (set-process-filter proc 'notmuch-search-process-filter))))
-    (run-hooks 'notmuch-search-hook)))
-
-(defun notmuch-search-refresh-view ()
-  "Refresh the current view.
-
-Kills the current buffer and runs a new search with the same
-query string as the current search. If the current thread is in
-the new search results, then point will be placed on the same
-thread. Otherwise, point will be moved to attempt to be in the
-same relative position within the new buffer."
-  (interactive)
-  (let ((here (point))
-       (oldest-first notmuch-search-oldest-first)
-       (thread (notmuch-search-find-thread-id))
-       (query notmuch-search-query-string))
-    (kill-this-buffer)
-    (notmuch-search query oldest-first)
-    (goto-char (point-min))
-    (if (re-search-forward (concat "^" thread) nil t)
-       (beginning-of-line)
-      (goto-char here))))
-
-(defun notmuch-search-toggle-order ()
-  "Toggle the current search order.
-
-By default, the \"inbox\" view created by `notmuch' is displayed
-in chronological order (oldest thread at the beginning of the
-buffer), while any global searches created by `notmuch-search'
-are displayed in reverse-chronological order (newest thread at
-the beginning of the buffer).
-
-This command toggles the sort order for the current search.
-
-Note that any filtered searches created by
-`notmuch-search-filter' retain the search order of the parent
-search."
-  (interactive)
-  (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
-  (notmuch-search-refresh-view))
-
-(defun notmuch-search-filter (query)
-  "Filter the current search results based on an additional query string.
-
-Runs a new search matching only messages that match both the
-current search results AND the additional query string provided."
-  (interactive "sFilter search: ")
-  (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query) (concat "( " query " )") query)))
-    (notmuch-search (concat notmuch-search-query-string " and " grouped-query) notmuch-search-oldest-first)))
-
-(defun notmuch-search-filter-by-tag (tag)
-  "Filter the current search results based on a single tag.
-
-Runs a new search matching only messages that match both the
-current search results AND that are tagged with the given tag."
-  (interactive
-   (list (notmuch-select-tag-with-completion "Filter by tag: ")))
-  (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
-
-
-;;;###autoload
-(defun notmuch ()
-  "Run notmuch to display all mail with tag of 'inbox'"
-  (interactive)
-  (notmuch-search "tag:inbox" notmuch-search-oldest-first))
-
-(setq mail-user-agent 'message-user-agent)
-
-(defvar notmuch-folder-mode-map
-  (let ((map (make-sparse-keymap)))
-    (define-key map "?" 'notmuch-help)
-    (define-key map "x" 'kill-this-buffer)
-    (define-key map "q" 'kill-this-buffer)
-    (define-key map ">" 'notmuch-folder-last)
-    (define-key map "<" 'notmuch-folder-first)
-    (define-key map "=" 'notmuch-folder)
-    (define-key map "s" 'notmuch-search)
-    (define-key map [mouse-1] 'notmuch-folder-show-search)
-    (define-key map (kbd "RET") 'notmuch-folder-show-search)
-    (define-key map "p" 'notmuch-folder-previous)
-    (define-key map "n" 'notmuch-folder-next)
-    map)
-  "Keymap for \"notmuch folder\" buffers.")
-
-(fset 'notmuch-folder-mode-map notmuch-folder-mode-map)
-
-(defcustom notmuch-folders (quote (("inbox" . "tag:inbox") ("unread" . "tag:unread")))
-  "List of searches for the notmuch folder view"
-  :type '(alist :key-type (string) :value-type (string))
-  :group 'notmuch)
-
-(defun notmuch-folder-mode ()
-  "Major mode for showing notmuch 'folders'.
-
-This buffer contains a list of message counts returned by a
-customizable set of searches of your email archives. Each line in
-the buffer shows the name of a saved search and the resulting
-message count.
-
-Pressing RET on any line opens a search window containing the
-results for the saved search on that line.
-
-Here is an example of how the search list could be
-customized, (the following text would be placed in your ~/.emacs
-file):
-
-(setq notmuch-folders '((\"inbox\" . \"tag:inbox\")
-                        (\"unread\" . \"tag:inbox AND tag:unread\")
-                        (\"notmuch\" . \"tag:inbox AND to:notmuchmail.org\")))
-
-Of course, you can have any number of folders, each configured
-with any supported search terms (see \"notmuch help search-terms\").
-
-Currently available key bindings:
-
-\\{notmuch-folder-mode-map}"
-  (interactive)
-  (kill-all-local-variables)
-  (use-local-map 'notmuch-folder-mode-map)
-  (setq truncate-lines t)
-  (hl-line-mode 1)
-  (setq major-mode 'notmuch-folder-mode
-       mode-name "notmuch-folder")
-  (setq buffer-read-only t))
-
-(defun notmuch-folder-next ()
-  "Select the next folder in the list."
-  (interactive)
-  (forward-line 1)
-  (if (eobp)
-      (forward-line -1)))
-
-(defun notmuch-folder-previous ()
-  "Select the previous folder in the list."
-  (interactive)
-  (forward-line -1))
-
-(defun notmuch-folder-first ()
-  "Select the first folder in the list."
-  (interactive)
-  (goto-char (point-min)))
-
-(defun notmuch-folder-last ()
-  "Select the last folder in the list."
-  (interactive)
-  (goto-char (point-max))
-  (forward-line -1))
-
-(defun notmuch-folder-add (folders)
-  (if folders
-      (let ((name (car (car folders)))
-           (inhibit-read-only t)
-           (search (cdr (car folders))))
-       (insert name)
-       (indent-to 16 1)
-       (call-process notmuch-command nil t nil "count" search)
-       (notmuch-folder-add (cdr folders)))))
-
-(defun notmuch-folder-find-name ()
-  (save-excursion
-    (beginning-of-line)
-    (let ((beg (point)))
-      (forward-word)
-      (filter-buffer-substring beg (point)))))
-
-(defun notmuch-folder-show-search (&optional folder)
-  "Show a search window for the search related to the specified folder."
-  (interactive)
-  (if (null folder)
-      (setq folder (notmuch-folder-find-name)))
-  (let ((search (assoc folder notmuch-folders)))
-    (if search
-       (notmuch-search (cdr search) notmuch-search-oldest-first))))
-
-;;;###autoload
-(defun notmuch-folder ()
-  "Show the notmuch folder view and update the displayed counts."
-  (interactive)
-  (let ((buffer (get-buffer-create "*notmuch-folders*")))
-    (switch-to-buffer buffer)
-    (let ((inhibit-read-only t)
-         (n (line-number-at-pos)))
-      (erase-buffer)
-      (notmuch-folder-mode)
-      (notmuch-folder-add notmuch-folders)
-      (goto-char (point-min))
-      (goto-line n))))
-
-(provide 'notmuch)
index 784981b6b190166564cfd82c2114bb1b1c3ec9d1..b1b61be40c2618fddcc56c46e683e517f9101f7d 100644 (file)
@@ -26,8 +26,6 @@ static void
 show_message_part (GMimeObject *part, int *part_count,
                   void (*show_part) (GMimeObject *part, int *part_count))
 {
-    *part_count = *part_count + 1;
-
     if (GMIME_IS_MULTIPART (part)) {
        GMimeMultipart *multipart = GMIME_MULTIPART (part);
        int i;
@@ -56,6 +54,8 @@ show_message_part (GMimeObject *part, int *part_count,
        return;
     }
 
+    *part_count = *part_count + 1;
+
     (*show_part) (part, part_count);
 }
 
@@ -102,3 +102,95 @@ show_message_body (const char *filename,
 
     return ret;
 }
+
+static void
+show_one_part_output (GMimeObject *part)
+{
+    GMimeStream *stream_filter = NULL;
+    GMimeDataWrapper *wrapper;
+    GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
+
+    stream_filter = g_mime_stream_filter_new(stream_stdout);
+    wrapper = g_mime_part_get_content_object (GMIME_PART (part));
+    if (wrapper && stream_filter)
+       g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+    if (stream_filter)
+       g_object_unref(stream_filter);
+}
+
+static void
+show_one_part_worker (GMimeObject *part, int *part_count, int desired_part)
+{
+    if (GMIME_IS_MULTIPART (part)) {
+       GMimeMultipart *multipart = GMIME_MULTIPART (part);
+       int i;
+
+       for (i = 0; i < g_mime_multipart_get_count (multipart); i++) {
+               show_one_part_worker (g_mime_multipart_get_part (multipart, i),
+                                     part_count, desired_part);
+       }
+       return;
+    }
+
+    if (GMIME_IS_MESSAGE_PART (part)) {
+       GMimeMessage *mime_message;
+
+       mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part));
+
+       show_one_part_worker (g_mime_message_get_mime_part (mime_message),
+                             part_count, desired_part);
+
+       return;
+    }
+
+    if (! (GMIME_IS_PART (part)))
+       return;
+
+    *part_count = *part_count + 1;
+
+    if (*part_count == desired_part)
+           show_one_part_output (part);
+}
+
+notmuch_status_t
+show_one_part (const char *filename, int part)
+{
+       GMimeStream *stream = NULL;
+       GMimeParser *parser = NULL;
+       GMimeMessage *mime_message = NULL;
+       notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+       FILE *file = NULL;
+       int part_count = 0;
+
+       file = fopen (filename, "r");
+       if (! file) {
+               fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+               ret = NOTMUCH_STATUS_FILE_ERROR;
+               goto DONE;
+       }
+
+       stream = g_mime_stream_file_new (file);
+       g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
+
+       parser = g_mime_parser_new_with_stream (stream);
+
+       mime_message = g_mime_parser_construct_message (parser);
+
+       show_one_part_worker (g_mime_message_get_mime_part (mime_message),
+                             &part_count, part);
+
+ DONE:
+       if (mime_message)
+               g_object_unref (mime_message);
+
+       if (parser)
+               g_object_unref (parser);
+
+       if (stream)
+               g_object_unref (stream);
+
+       if (file)
+               fclose (file);
+
+       return ret;
+}
diff --git a/test/notmuch-test b/test/notmuch-test
new file mode 100755 (executable)
index 0000000..1c5191b
--- /dev/null
@@ -0,0 +1,544 @@
+#!/bin/bash
+set -e
+
+find_notmuch_binary ()
+{
+    dir=$1
+
+    while [ -n "$dir" ]; do
+       bin=$dir/notmuch
+       if [ -x $bin ]; then
+           echo $bin
+           return
+       fi
+       dir=$(dirname $dir)
+       if [ "$dir" = "/" ]; then
+           break
+       fi
+    done
+
+    echo notmuch
+}
+
+increment_mtime_amount=0
+increment_mtime ()
+{
+    dir=$1
+
+    increment_mtime_amount=$((increment_mtime_amount + 1))
+    touch -d "+${increment_mtime_amount} seconds" $dir
+}
+
+# Generate a new message in the mail directory, with a unique message
+# ID and subject. The message is not added to the index.
+#
+# After this function returns, the filename of the generated message
+# is available as $gen_msg_filename and the message ID is available as
+# $gen_msg_id .
+#
+# This function supports named parameters with the bash syntax for
+# assigning a value to an associative array ([name]=value). The
+# supported parameters are:
+#
+#  [dir]=directory/of/choice
+#
+#      Generate the message in directory 'directory/of/choice' within
+#      the mail store. The directory will be created if necessary.
+#
+#  [body]=text
+#
+#      Text to use as the body of the email message
+#
+#  '[from]="Some User <user@example.com>"'
+#  '[to]="Some User <user@example.com>"'
+#  '[subject]="Subject of email message"'
+#  '[date]="RFC 822 Date"'
+#
+#      Values for email headers. If not provided, default values will
+#      be generated instead.
+#
+#  '[cc]="Some User <user@example.com>"'
+#  [reply-to]=some-address
+#  [in-reply-to]=<message-id>
+#
+#      Additional values for email headers. If these are not provided
+#      then the relevant headers will simply not appear in the
+#      message.
+gen_msg_cnt=0
+gen_msg_filename=""
+gen_msg_id=""
+generate_message ()
+{
+    # This is our (bash-specific) magic for doing named parameters
+    local -A template="($@)"
+    local additional_headers
+
+    gen_msg_cnt=$((gen_msg_cnt + 1))
+    gen_msg_name=msg-$(printf "%03d" $gen_msg_cnt)
+    gen_msg_id="${gen_msg_name}@notmuch-test-suite"
+
+    if [ -z "${template[dir]}" ]; then
+       gen_msg_filename="${MAIL_DIR}/$gen_msg_name"
+    else
+       gen_msg_filename="${MAIL_DIR}/${template[dir]}/$gen_msg_name"
+       mkdir -p $(dirname $gen_msg_filename)
+    fi
+
+    if [ -z "${template[body]}" ]; then
+       template[body]="This is just a test message at ${gen_msg_filename}"
+    fi
+
+    if [ -z "${template[from]}" ]; then
+       template[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+    fi
+
+    if [ -z "${template[to]}" ]; then
+       template[to]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+    fi
+
+    if [ -z "${template[subject]}" ]; then
+       template[subject]="Test message ${gen_msg_filename}"
+    fi
+
+    if [ -z "${template[date]}" ]; then
+       template[date]="Tue, 05 Jan 2010 15:43:57 -0800"
+    fi
+
+    additional_headers=""
+    if [ ! -z "${template[reply-to]}" ]; then
+       additional_headers="Reply-To: ${template[reply-to]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[in-reply-to]}" ]; then
+       additional_headers="In-Reply-To: ${template[in-reply-to]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[cc]}" ]; then
+       additional_headers="Cc: ${template[cc]}
+${additional_headers}"
+    fi
+
+cat <<EOF >$gen_msg_filename
+From: ${template[from]}
+To: ${template[to]}
+Message-Id: <${gen_msg_id}>
+Subject: ${template[subject]}
+Date: ${template[date]}
+${additional_headers}
+${template[body]}
+EOF
+
+    # Ensure that the mtime of the containing directory is updated
+    increment_mtime $(dirname ${gen_msg_filename})
+}
+
+# Generate a new message and add it to the index.
+#
+# All of the arguments and return values supported by generate_message
+# are alos supported here, so see that function for details.
+add_message ()
+{
+    generate_message "$@"
+
+    $NOTMUCH new > /dev/null
+}
+
+NOTMUCH_IGNORED_OUTPUT_REGEXP='^Processed [0-9]*( total)? file|Found [0-9]* total file'
+NOTMUCH_THREAD_ID_SQUELCH='s/thread:................/thread:XXX/'
+execute_expecting ()
+{
+    args=$1
+    expected=$2
+
+    output=$($NOTMUCH $args | grep -v -E -e "$NOTMUCH_IGNORED_OUTPUT_REGEXP" | sed -e "$NOTMUCH_THREAD_ID_SQUELCH" || true)
+    if [ "$output" = "$expected" ]; then
+       echo "  PASS"
+    else
+       echo "  FAIL"
+       echo "  Expected output: $expected"
+       echo "  Actual output:   $output"
+    fi
+}
+
+TEST_DIR=$(pwd)/test.$$
+MAIL_DIR=${TEST_DIR}/mail
+export NOTMUCH_CONFIG=${TEST_DIR}/notmuch-config
+NOTMUCH=$(find_notmuch_binary $(pwd))
+
+rm -rf ${TEST_DIR}
+mkdir ${TEST_DIR}
+cd ${TEST_DIR}
+
+mkdir ${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
+EOF
+
+printf "Testing \"notmuch new\" in several variations:\n"
+printf " No new messages...\t\t"
+execute_expecting new "No new mail."
+
+printf " Single new message...\t\t"
+generate_message
+execute_expecting new "Added 1 new message to the database."
+
+printf " Multiple new messages...\t"
+generate_message
+generate_message
+execute_expecting new "Added 2 new messages to the database."
+
+printf " No new messages (non-empty DB)... "
+execute_expecting new "No new mail."
+
+printf " New directories...\t\t"
+rm -rf ${MAIL_DIR}/* ${MAIL_DIR}/.notmuch
+mkdir ${MAIL_DIR}/def
+mkdir ${MAIL_DIR}/ghi
+generate_message [dir]=def
+
+execute_expecting new "Added 1 new message to the database."
+
+printf " Alternate inode order...\t"
+
+rm -rf ${MAIL_DIR}/.notmuch
+mv ${MAIL_DIR}/ghi ${MAIL_DIR}/abc
+rm ${MAIL_DIR}/def/*
+generate_message [dir]=abc
+
+execute_expecting new "Added 1 new message to the database."
+
+printf " Message moved in...\t\t"
+rm -rf ${MAIL_DIR}/* ${MAIL_DIR}/.notmuch
+generate_message
+tmp_msg_filename=tmp/$gen_msg_filename
+mkdir -p $(dirname $tmp_msg_filename)
+mv $gen_msg_filename $tmp_msg_filename
+increment_mtime ${MAIL_DIR}
+$NOTMUCH new > /dev/null
+mv $tmp_msg_filename $gen_msg_filename
+increment_mtime ${MAIL_DIR}
+execute_expecting new "Added 1 new message to the database."
+
+printf " Renamed message...\t\t"
+
+generate_message
+$NOTMUCH new > /dev/null
+mv $gen_msg_filename ${gen_msg_filename}-renamed
+increment_mtime ${MAIL_DIR}
+execute_expecting new "No new mail. Detected 1 file rename."
+
+printf " Deleted message...\t\t"
+
+rm ${gen_msg_filename}-renamed
+increment_mtime ${MAIL_DIR}
+execute_expecting new "No new mail. Removed 1 message."
+
+printf " Renamed directory...\t\t"
+
+generate_message [dir]=dir
+generate_message [dir]=dir
+generate_message [dir]=dir
+
+$NOTMUCH new > /dev/null
+
+mv ${MAIL_DIR}/dir ${MAIL_DIR}/dir-renamed
+increment_mtime ${MAIL_DIR}
+
+execute_expecting new "No new mail. Detected 3 file renames."
+
+printf " Deleted directory...\t\t"
+
+rm -rf ${MAIL_DIR}/dir-renamed
+increment_mtime ${MAIL_DIR}
+
+execute_expecting new "No new mail. Removed 3 messages."
+
+printf " New directory (at end of list)... "
+
+generate_message [dir]=zzz
+generate_message [dir]=zzz
+generate_message [dir]=zzz
+
+execute_expecting new "Added 3 new messages to the database."
+
+printf " Deleted directory (end of list)... "
+
+rm -rf ${MAIL_DIR}/zzz
+increment_mtime ${MAIL_DIR}
+
+execute_expecting new "No new mail. Removed 3 messages."
+
+printf " New symlink to directory...\t"
+
+rm -rf ${MAIL_DIR}/.notmuch
+mv ${MAIL_DIR} ${TEST_DIR}/actual_maildir
+
+mkdir ${MAIL_DIR}
+ln -s ${TEST_DIR}/actual_maildir ${MAIL_DIR}/symlink
+
+execute_expecting new "Added 1 new message to the database."
+
+printf " New symlink to a file...\t"
+generate_message
+external_msg_filename=${TEST_DIR}/external/$(basename $gen_msg_filename)
+mkdir -p $(dirname $external_msg_filename)
+mv $gen_msg_filename $external_msg_filename
+ln -s $external_msg_filename $gen_msg_filename
+increment_mtime ${MAIL_DIR}
+execute_expecting new "Added 1 new message to the database."
+
+printf " New two-level directory...\t"
+
+generate_message [dir]=two/levels
+generate_message [dir]=two/levels
+generate_message [dir]=two/levels
+
+execute_expecting new "Added 3 new messages to the database."
+
+printf " Deleted two-level directory... "
+
+rm -rf ${MAIL_DIR}/two
+increment_mtime ${MAIL_DIR}
+
+execute_expecting new "No new mail. Removed 3 messages."
+
+printf "\nTesting \"notmuch search\" in several variations:\n"
+
+printf " Search body...\t\t\t"
+add_message '[subject]="body search"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [body]=bodysearchtest
+execute_expecting "search bodysearchtest" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; body search (inbox unread)"
+
+printf " Search by from:...\t\t"
+add_message '[subject]="search by from"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom
+execute_expecting "search from:searchbyfrom" "thread:XXX   2000-01-01 [1/1] searchbyfrom; search by from (inbox unread)"
+
+printf " Search by to:...\t\t"
+add_message '[subject]="search by to"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto
+execute_expecting "search to:searchbyto" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread)"
+
+printf " Search by subject:...\t\t"
+add_message [subject]=subjectsearchtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+execute_expecting "search subject:subjectsearchtest" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread)"
+
+printf " Search by id:...\t\t"
+add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+execute_expecting "search id:${gen_msg_id}" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)"
+
+printf " Search by tag:...\t\t"
+add_message '[subject]="search by tag"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+$NOTMUCH tag +searchbytag id:${gen_msg_id}
+execute_expecting "search tag:searchbytag" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)"
+
+printf " Search by thread:...\t\t"
+add_message '[subject]="search by thread"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+thread_id=$($NOTMUCH search id:${gen_msg_id} | sed -e 's/thread:\([a-f0-9]*\).*/\1/')
+execute_expecting "search thread:${thread_id}" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by thread (inbox unread)"
+
+printf " Search body (phrase)...\t"
+add_message '[subject]="body search (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="body search (phrase)"'
+add_message '[subject]="negative result"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="This phrase should not match the body search"'
+execute_expecting "search '\"body search (phrase)\"'" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; body search (phrase) (inbox unread)"
+
+printf " Search by from: (address)...\t"
+add_message '[subject]="search by from (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom@example.com
+execute_expecting "search from:searchbyfrom@example.com" "thread:XXX   2000-01-01 [1/1] searchbyfrom@example.com; search by from (address) (inbox unread)"
+
+printf " Search by from: (name)...\t"
+add_message '[subject]="search by from (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[from]="Search By From Name <test@example.com>"'
+execute_expecting "search from:'Search By From Name'" "thread:XXX   2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)"
+
+printf " Search by to: (address)...\t"
+add_message '[subject]="search by to (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto@example.com
+execute_expecting "search to:searchbyto@example.com" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (address) (inbox unread)"
+
+printf " Search by to: (name)...\t"
+add_message '[subject]="search by to (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[to]="Search By To Name <test@example.com>"'
+execute_expecting "search to:'Search By To Name'" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)"
+
+printf " Search by subject: (phrase)...\t"
+add_message '[subject]="subject search test (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+add_message '[subject]="this phrase should not match the subject search test"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+execute_expecting "search 'subject:\"subject search test (phrase)\"'" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; subject search test (phrase) (inbox unread)"
+
+printf "\nTesting \"notmuch reply\" in several variations:\n"
+
+printf " Basic reply...\t\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+             [to]=test_suite@notmuchmail.org \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+            '[body]="basic reply test"'
+
+execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References:  <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> basic reply test"
+
+printf " Multiple recipients...\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+            '[to]="test_suite@notmuchmail.org, Someone Else <someone@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+            '[body]="Multiple recipients"'
+
+execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, Someone Else <someone@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References:  <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> Multiple recipients"
+
+printf " Reply with CC...\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+             [to]=test_suite@notmuchmail.org \
+            '[cc]="Other Parties <cc@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+            '[body]="reply with CC"'
+
+execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Cc: Other Parties <cc@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References:  <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> reply with CC"
+
+printf " Reply from alternate address..."
+add_message '[from]="Sender <sender@example.com>"' \
+             [to]=test_suite_other@notmuchmail.org \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+            '[body]="reply from alternate address"'
+
+execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References:  <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> reply from alternate address"
+
+printf " Support for Reply-To...\t"
+add_message '[from]="Sender <sender@example.com>"' \
+             [to]=test_suite@notmuchmail.org \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+            '[body]="support for reply-to"' \
+            '[reply-to]="Sender <elsewhere@example.com>"'
+
+execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <elsewhere@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References:  <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> support for reply-to"
+
+printf " Un-munging Reply-To...\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+            '[to]="Some List <list@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+            '[body]="Un-munging Reply-To"' \
+            '[reply-to]="Evil Munging List <list@example.com>"'
+
+execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, Some List <list@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References:  <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> Un-munging Reply-To"
+
+printf "\nTesting handling of uuencoded data:\n"
+
+add_message [subject]=uuencodetest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \
+'[body]="This message is used to ensure that notmuch correctly handles a
+message containing a block of uuencoded data. First, we have a marker
+this content beforeuudata . Then we beging the uunencoded data itself:
+
+begin 644 bogus-uuencoded-data
+M0123456789012345678901234567890123456789012345678901234567890
+MOBVIOUSLY, THIS IS NOT ANY SORT OF USEFUL UUNECODED DATA.    
+MINSTEAD THIS IS JUST A WAY TO ENSURE THAT THIS BLOCK OF DATA 
+MIS CORRECTLY IGNORED WHEN NOTMUCH CREATES ITS INDEX. SO WE   
+MINCLUDE A DURINGUUDATA MARKER THAT SHOULD NOT RESULT IN ANY  
+MSEARCH RESULT.                                               
+\`
+end
+
+Finally, we have our afteruudata marker as well."'
+
+printf " Ensure content before uu data is indexed..."
+execute_expecting "search beforeuudata" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
+printf " Ensure uu data is not indexed...\t"
+execute_expecting "search DURINGUUDATA" ""
+printf " Ensure content after uu data is indexed..."
+execute_expecting "search afteruudata" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
+
+printf "\nTesting \"notmuch dump\" and \"notmuch restore\":\n"
+
+printf " Dumping all tags...\t\t"
+$NOTMUCH dump dump.expected
+echo " PASS"
+
+printf " Clearing all tags...\t\t"
+sed -e 's/(\([^(]*\))$/()/' < dump.expected > clear.expected
+$NOTMUCH restore clear.expected
+$NOTMUCH dump clear.actual
+if diff clear.expected clear.actual > /dev/null; then
+    echo "     PASS"
+else
+    echo "     FAIL"
+    echo "     Expected output: See file clear.expected"
+    echo "     Actual output:   See file clear.actual"
+fi
+
+printf " Restoring original tags...\t"
+$NOTMUCH restore dump.expected
+$NOTMUCH dump dump.actual
+if diff dump.expected dump.actual > /dev/null; then
+    echo "     PASS"
+else
+    echo "     FAIL"
+    echo "     Expected output: See file dump.expected"
+    echo "     Actual output:   See file dump.actual"
+fi
+
+printf " Restore with nothing to do...\t"
+$NOTMUCH restore dump.expected
+echo " PASS"
+
+cat <<EOF
+Notmuch test suite complete.
+
+Intermediate state can be examined in:
+       ${TEST_DIR}
+EOF