Merge commit '0.11.1'
authorDavid Bremner <bremner@debian.org>
Sat, 4 Feb 2012 17:01:46 +0000 (12:01 -0500)
committerDavid Bremner <bremner@debian.org>
Sat, 4 Feb 2012 17:01:46 +0000 (12:01 -0500)
Conflicts:
NEWS
bindings/python/notmuch/database.py
bindings/python/notmuch/message.py
notmuch.1

NEWS merged by hand, others taken from master.

107 files changed:
.dir-locals.el
.gitignore
INSTALL
Makefile
Makefile.local
NEWS
RELEASING [deleted file]
TODO [deleted file]
bindings/python/notmuch.py
bindings/python/notmuch/database.py
bindings/python/notmuch/filename.py
bindings/python/notmuch/globals.py
bindings/python/notmuch/message.py
bindings/python/notmuch/tag.py
bindings/python/notmuch/thread.py
bindings/python/setup.py
compat/Makefile.local
compat/compat.h
configure
debian/notmuch.install
devel/RELEASING [new file with mode: 0644]
devel/TODO [new file with mode: 0644]
devel/uncrustify.cfg [new file with mode: 0644]
emacs/Makefile.local
emacs/notmuch-address.el
emacs/notmuch-crypto.el
emacs/notmuch-hello.el
emacs/notmuch-lib.el
emacs/notmuch-maildir-fcc.el
emacs/notmuch-message.el
emacs/notmuch-mua.el
emacs/notmuch-print.el [new file with mode: 0644]
emacs/notmuch-show.el
emacs/notmuch-wash.el
emacs/notmuch.el
lib/database.cc
lib/index.cc
lib/messages.c
lib/notmuch-private.h
lib/notmuch.h
lib/query.cc
man/.gitignore [new file with mode: 0644]
man/Makefile [new file with mode: 0644]
man/Makefile.local [new file with mode: 0644]
man/man1/notmuch-config.1 [new file with mode: 0644]
man/man1/notmuch-count.1 [new file with mode: 0644]
man/man1/notmuch-dump.1 [new file with mode: 0644]
man/man1/notmuch-new.1 [new file with mode: 0644]
man/man1/notmuch-reply.1 [new file with mode: 0644]
man/man1/notmuch-restore.1 [new file with mode: 0644]
man/man1/notmuch-search.1 [new file with mode: 0644]
man/man1/notmuch-setup.1 [new symlink]
man/man1/notmuch-show.1 [new file with mode: 0644]
man/man1/notmuch-tag.1 [new file with mode: 0644]
man/man1/notmuch.1 [new file with mode: 0644]
man/man5/notmuch-hooks.5 [new file with mode: 0644]
man/man7/notmuch-search-terms.7 [new file with mode: 0644]
mime-node.c [new file with mode: 0644]
notmuch-client.h
notmuch-config.c
notmuch-count.c
notmuch-new.c
notmuch-reply.c
notmuch-search.c
notmuch-setup.c
notmuch-show.c
notmuch-tag.c
notmuch.1 [deleted file]
notmuch.c
show-message.c
test/Makefile.local
test/README
test/corpus/cur/52:2, [new file with mode: 0644]
test/corpus/cur/53:2, [new file with mode: 0644]
test/crypto
test/emacs
test/emacs-address-cleaning [new file with mode: 0755]
test/emacs-address-cleaning.el [new file with mode: 0644]
test/emacs-large-search-buffer
test/emacs-show [new file with mode: 0755]
test/emacs-subject-to-filename [new file with mode: 0755]
test/emacs-test-functions [new file with mode: 0755]
test/emacs.expected-output/notmuch-hello
test/emacs.expected-output/notmuch-hello-no-saved-searches
test/emacs.expected-output/notmuch-hello-view-inbox
test/emacs.expected-output/notmuch-hello-with-empty
test/emacs.expected-output/notmuch-search-tag-inbox
test/emacs.expected-output/notmuch-show-thread-maildir-storage
test/emacs.expected-output/notmuch-show-thread-maildir-storage-with-fourfold-indentation
test/emacs.expected-output/notmuch-show-thread-maildir-storage-without-indentation
test/encoding
test/from-guessing
test/maildir-sync
test/multipart
test/notmuch-test
test/python
test/raw
test/reply
test/reply-to-sender [new file with mode: 0755]
test/search
test/search-output
test/search-position-overlap-bug
test/symbol-hiding
test/test-lib.el
test/test-lib.sh
test/thread-naming
util/Makefile.local

index aea630bdf3155b26113881e36a45a83d1e82bd4d..fc75ae615bd5a73b25eedabdfe0619286c0436b3 100644 (file)
@@ -1,6 +1,6 @@
-; emacs local configuration settings for notmuch source
-; surmised by dkg on 2010-11-23 13:43:18-0500
-; amended by amdragon on 2011-06-06
+;; emacs local configuration settings for notmuch source
+;; surmised by dkg on 2010-11-23 13:43:18-0500
+;; amended by amdragon on 2011-06-06
 
 ((c-mode
   (indent-tabs-mode . t)
@@ -20,4 +20,6 @@
   (tab-width . 8)
   (sh-basic-offset . 4)
   (sh-indentation . 4))
+ (nil
+  (fill-column . 70))
  )
index d64ec9f35962bc957beb777151849e5bde7bf6c1..d4282909e319ddfef39666e41eff06c60590a8fb 100644 (file)
@@ -7,7 +7,6 @@ tags
 /notmuch
 notmuch.sym
 notmuch-shared
-notmuch.1.gz
 libnotmuch.so*
 libnotmuch*.dylib
 *.[ao]
diff --git a/INSTALL b/INSTALL
index e51b397cc2a9d77a56424074775dfcdaf4b5abfb..bc98f1dec86ac2989aa1d67d402e725c33415233 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -20,8 +20,8 @@ configure stage.
 
 Dependencies
 ------------
-Notmuch depends on three libraries: Xapian, GMime 2.4, and Talloc
-which are each described below:
+Notmuch depends on three libraries: Xapian, GMime 2.4 or 2.6, and
+Talloc which are each described below:
 
        Xapian
        ------
@@ -39,14 +39,14 @@ which are each described below:
        reading mail while notmuch would wait for Xapian when removing
        the "inbox" and "unread" tags from messages in a thread.
 
-       GMime 2.4
-       ---------
-       GMime 2.4 provides decoding of MIME email messages for Notmuch.
+       GMime 2.4 or 2.6
+       ----------------
+       GMime provides decoding of MIME email messages for Notmuch.
 
        Without GMime, Notmuch would not be able to extract and index
        the actual text from email message encoded as BASE64, etc.
 
-       GMime 2.4 is available from http://spruce.sourceforge.net/gmime/
+       GMime is available from http://spruce.sourceforge.net/gmime/
 
        Talloc
        ------
index 2fb2a61354c7bbd3ca75219150e7ebaf1b0e7944..e5e2e3a3ac67a9e515411c7e03d449db59d9a5c0 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
 all:
 
 # List all subdirectories here. Each contains its own Makefile.local
-subdirs = compat completion emacs lib util test
+subdirs = compat completion emacs lib man util test
 
 # We make all targets depend on the Makefiles themselves.
 global_deps = Makefile Makefile.config Makefile.local \
index 97f397ff2d238ca99bbb735488ccd06dd0c40d4b..1131dea8ebc90bfd21cc71283560d1f82b7b50f1 100644 (file)
@@ -53,7 +53,7 @@ endif
 FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(AS_NEEDED_LDFLAGS) $(CONFIGURE_LDFLAGS)
 
 .PHONY: all
-all: notmuch notmuch-shared notmuch.1.gz
+all: notmuch notmuch-shared
 ifeq ($(MAKECMDGOALS),)
 ifeq ($(shell cat .first-build-message 2>/dev/null),)
        @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
@@ -95,8 +95,7 @@ dist: $(TAR_FILE)
 
 .PHONY: update-versions
 
-update-versions:
-       sed -i "s/^.TH NOTMUCH 1.*$$/.TH NOTMUCH 1 ${DATE} \"Notmuch ${VERSION}\"/" notmuch.1
+update-versions: update-man-versions
        sed -i "s/^__VERSION__[[:blank:]]*=.*$$/__VERSION__ = \'${VERSION}\'/" $(PV_FILE)
 
 # We invoke make recursively only to force ordering of our phony
@@ -221,14 +220,6 @@ verify-version-python: verify-version-components
                 echo "Please edit version and $(PV_FILE) to have consistent versions." && false)
        @echo "Good."
 
-.PHONY: verify-version-manpage
-verify-version-manpage: verify-version-components
-       @echo -n "Checking that manual page version is $(VERSION)..."
-       @[ "$(VERSION)" = $$(sed -n '/^[.]TH NOTMUCH 1/{s/.*"Notmuch //;s/".*//p;}' notmuch.1) ] || \
-               (echo "No." && \
-                echo "Please edit version and notmuch.1 to have consistent versions." && false)
-       @echo "Good."
-
 .PHONY: verify-version-components
 verify-version-components:
        @echo -n "Checking that $(VERSION) consists only of digits and periods..."
@@ -282,10 +273,6 @@ quiet ?= $($(shell echo $1 | sed -e s'/ .*//'))
        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
@@ -315,6 +302,7 @@ notmuch_client_srcs =               \
        notmuch-time.c          \
        query-string.c          \
        show-message.c          \
+       mime-node.c             \
        json.c
 
 notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
@@ -325,13 +313,8 @@ notmuch: $(notmuch_client_modules) lib/libnotmuch.a util/libutil.a
 notmuch-shared: $(notmuch_client_modules) lib/$(LINKER_NAME)
        $(call quiet,$(FINAL_NOTMUCH_LINKER) $(CFLAGS)) $(notmuch_client_modules) $(FINAL_NOTMUCH_LDFLAGS) -o $@
 
-notmuch.1.gz: notmuch.1
-       gzip --stdout $^ > $@
-
 .PHONY: install
-install: all notmuch.1.gz
-       mkdir -p "$(DESTDIR)$(mandir)/man1"
-       install -m0644 notmuch.1.gz "$(DESTDIR)$(mandir)/man1/"
+install: all install-man
        mkdir -p "$(DESTDIR)$(prefix)/bin/"
        install notmuch-shared "$(DESTDIR)$(prefix)/bin/notmuch"
 ifeq ($(MAKECMDGOALS), install)
@@ -362,4 +345,8 @@ install-desktop:
        desktop-file-install --mode 0644 --dir "$(DESTDIR)$(desktop_dir)" notmuch.desktop
 
 SRCS  := $(SRCS) $(notmuch_client_srcs)
-CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) notmuch.elc notmuch.1.gz
+CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) notmuch.elc
+
+DEPS := $(SRCS:%.c=.deps/%.d)
+DEPS := $(DEPS:%.cc=.deps/%.d)
+-include $(DEPS)
diff --git a/NEWS b/NEWS
index e57e9708a26826f5c854a4e28ca0185a7e446985..5c5b6458262bcdd7692b2253c5663d2183f227b8 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,61 @@
+Notmuch 0.12 (2012-xx-xx)
+=========================
+
+Command-Line Interface
+----------------------
+
+Reply to sender
+
+  "notmuch reply" has gained the ability to create a reply template
+  for replying just to the sender of the message, in addition to reply
+  to all. The feature is available through the new command line option
+  --reply-to=(all|sender).
+
+Tag exclusion
+
+  Tags can be automatically excluded from search results by adding them
+  to the new 'search.exclude_tags' option in the Notmuch config file.
+
+  This behaviour can be overridden by explicitly including an excluded
+  tag in your query, for example:
+
+    notmuch search $your_query and tag:$excluded_tag
+
+  Existing users will probably want to run "notmuch setup" again to add
+  the new well-commented [search] section to the configuration file.
+
+  For new configurations, accepting the default setting will cause the
+  tags "deleted" and "spam" to be excluded, equivalent to running:
+
+    notmuch config set search.exclude_tags deleted spam
+
+Emacs Interface
+---------------
+
+Reply to sender
+
+  The Emacs interface has, with the new CLI support, gained the
+  ability to reply to sender in addition to reply to all. In both show
+  and search modes, 'r' has been bound to reply to sender, replacing
+  reply to all, which now has key binding 'R'.
+
+Library changes
+---------------
+
+New functions
+
+  notmuch_query_add_tag_exclude supports the new tag exclusion
+  feature.
+
+Build fixes
+-----------
+
+Compatibility with GMime 2.6
+
+  It is now possible to build notmuch against both GMime 2.4 and 2.6.
+  However, a bug in current GMime 2.6 causes notmuch not to report
+  signatures where the signer key is unavailable (GNOME bug 668085).
+
 Notmuch 0.11.1 (2012-02-03)
 ===========================
 
@@ -22,7 +80,6 @@ Quote MML tags in replies
   outgoing message.  The Emacs interface now quotes these tags in
   reply text, so that they do not effect outgoing messages.
 
-
 Notmuch 0.11 (2012-01-13)
 =========================
 
@@ -135,8 +192,8 @@ Bug-fix release.
 
 Fix crash in python bindings.
 
-    The python bindings did not call g_type_init, which caused crashes
-    for some, but not all users.
+  The python bindings did not call g_type_init, which caused crashes
+  for some, but not all users.
 
 Notmuch 0.10.1 (2011-11-25)
 ===========================
@@ -206,8 +263,8 @@ Add keybinding ('c I') for stashing Message-ID's without an id: prefix
 
 Do not query on notmuch-search exit
 
-   It is harmless to kill the external notmuch process, so the user
-   is no longer interrogated when they interrupt a search.
+  It is harmless to kill the external notmuch process, so the user
+  is no longer interrogated when they interrupt a search.
 
 Performance
 -----------
@@ -236,9 +293,9 @@ mailing list.
 
 nmbug - share tags with a given prefix
 
-   nmbug helps maintain a git repo containing all tags with a given
-   prefix (by default "notmuch::"). Tags can be shared by commiting
-   them to git in one location and restoring in another.
+  nmbug helps maintain a git repo containing all tags with a given
+  prefix (by default "notmuch::"). Tags can be shared by commiting
+  them to git in one location and restoring in another.
 
 Notmuch 0.9 (2011-10-01)
 ========================
@@ -623,7 +680,7 @@ Ruby bindings are now much more complete
        s1.union(s2)
        s2 -= s1
 
-   Removed:
+  Removed:
      - len(Messages()) as it exhausted the iterator.
        Use len(list(Messages())) or
        Query.count_messages() to get the length.
diff --git a/RELEASING b/RELEASING
deleted file mode 100644 (file)
index 88dab04..0000000
--- a/RELEASING
+++ /dev/null
@@ -1,113 +0,0 @@
-Here are the steps to follow to create a new notmuch release.
-
-These steps assume that a process (not described here) has already
-been followed to determine the features and bug fixes to be included
-in a release, and that adequate testing by the community has already
-been performed. The little bit of testing performed here is a safety
-check, and not a substitute for wider testing.
-
-OK, so the code to be released is present and committed to your git
-repository. From here, there are just a few steps to release:
-
-1) 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, please add them, (and ask the
-       authors of the commits to update NEWS in the future).
-
-2) Verify that the library version in lib/Makefile.local is correct
-
-       See the instructions there for how to increment it.
-
-       The version should have been updated with any commits that
-       added API _in a non-upwardly compatible_ way, but do check
-       that that is the case. The command below can be useful for
-       inspecting header-file changes since the last release X.Y:
-
-               git diff X.Y..HEAD -- lib/notmuch.h
-
-       Commit this change, if any.
-
-3) Update the debian/libnotmuchX.symbols file
-
-       If the library version changed at all (step 2) it probably
-       means that symbols have changed/been added, in which case the
-       debian symbols file also needs to be updated:
-
-              dpkg-buildpackage -uc -us
-              dpkg-gensymbols -plibnotmuchX | patch -p0
-
-       Carefully review the changes to debian/libnotmuch1.symbols to
-       make sure there are no unexpected changes.  Remove any debian
-       versions from symbols.
-
-       Commit this change, if any.
-
-4) Upgrade the version in the file "version"
-
-       The scheme for the release number is as follows:
-
-       A major milestone in usability causes an increase in the major
-       number, yielding a two-component version with a minor number
-       of 0, (such as "1.0" or "2.0").
-
-       Otherwise, releases with changes in features cause an increase
-       in the minor number, yielding a two-component version, (such
-       as "1.1" or "1.2").
-
-       Finally, releases that do not change "features" but are merely
-       bug fixes either increase the micro number or add it (starting
-       at ".1" if not present). So a bug-fix release from "1.0" would
-       be "1.0.1" and a subsequent bug-fix release would be "1.0.2"
-       etc.
-
-       When you are happy with the file 'version', run
-
-            make update-versions
-
-       to propagate the version to the other places needed.
-
-       Commit these changes.
-
-5) Create an entry for the new release in debian/changelog
-
-       The syntax of this file is tightly restricted, but the
-       available emacs mode (see the dpkg-dev-el package) helps.
-       The entries here will be the Debian-relevant single-line
-       description of changes from the NEWS entry. And the version
-       must match the version in the next step.
-
-       Commit this change.
-
-       XXX: It would be great if this step were automated as part of
-       release, (taking entries from NEWS and the version from the
-       version file, and creating a new commit, etc.)
-
-6) Run "make release" which will perform the following steps.
-
-   Note: in order to really upload anything, set the make variable
-   REALLY_UPLOAD=yes
-
-       * Ensure that the version consists only of digits and periods
-       * Ensure that version and debian/changelog have the same version
-       * Verify that the source tree is clean
-       * Compile the current notmuch code (aborting release if it fails)
-       * Run the notmuch test suite (aborting release if it fails)
-       * Check that no release exists with the current version
-       * Make a signed tag
-       * Generate a tar file from this tag
-       * Generate a .sha1 sum file for the tar file and GPG sign it.
-       * Commit a (delta for a) copy of the tar file using pristine-tar
-       * Tag for the debian version
-       * if REALLY_UPLOAD=yes
-         - push the signed tag to the origin
-           XXX FIXME push debian tag
-         - scp tarball to web site
-       * Provide some text for the release announcement (see below).
-
-7) Send a message to notmuch@notmuchmail.org to announce the release.
-
-       Use the text provided from "make release" above, (if for some
-       reason you lose this message, "make release-message" prints
-       it again for you.
diff --git a/TODO b/TODO
deleted file mode 100644 (file)
index 4dda6f4..0000000
--- a/TODO
+++ /dev/null
@@ -1,285 +0,0 @@
-Fix the things that are causing the most pain to new users
-----------------------------------------------------------
-1. A new import is tagging all messages as "inbox" -- total pain
-
-Emacs interface (notmuch.el)
-----------------------------
-Add notmuch-bcc and notmuch-cc for setting default Bcc and Cc values,
-(should affect the message-setup-hook).
-
-Switch the notmuch-search view to use "notmuch search --format=json"
-to fix large classes of bugs regarding poorly-escaped output and lame
-regular expressions. (The most recently found, unfixed example is the
-sender's name containing ';' which causes emacs to drop a search
-result.) This may require removing the outer array from the current
-"notmuch search --format=json" results.
-
-Fix '*' to work by simply calling '+' or '-' on a region consisting of
-the entire buffer, (this would avoid one race condition---while still
-leaving other race conditions---but could also potentially make '*' a
-very expensive operation).
-
-Add a global keybinding table for notmuch, and then view-specific
-tables that add to it.
-       
-Add a '|' binding from the search view.
-
-Add support for choosing from one of the user's configured email
-addresses for the From line.
-
-Make 'notmuch-show-pipe-message have a private history.
-
-Add support for a delete keybinding that adds a "deleted" tag to the
-current message/thread and make searches not return deleted messages
-by default, (unless the user asks explicitly for deleted messages in
-the search query).
-
-Add keybindings for next/previous thread.
-
-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).
-
-Make '=' count from the end rather than from the beginning if more
-than half-way through the buffer.
-
-Fix to automatically wrap long headers (for RFC compliance) before
-sending. This should probably just be fixed in message-mode itself,
-(but perhaps we can have a notmuch-message-mode that layers this on
-top).
-
-Stop hiding the headers so much in the thread-view mode.
-
-Allow opening a message in thread-view mode by clicking on either
-line.
-
-Automatically open a message when navigating to it with N or P.
-
-Change 'a' command in thread-view mode to only archive open messages.
-
-Add a binding to open all closed messages.
-
-Change the 'a'rchive command in the thread view to only archive open
-messages.
-
-Completion
-----------
-Fix bash completion to complete multiple search options (both --first
-and *then* --max-threads), and also complete value for --sort=
-(oldest-first or newest-first).
-
-notmuch command-line tool
--------------------------
-Add support to "notmuch search" and "notmuch show" to allow for
-listing of duplicate messages, (distinct filenames with the same
-Message-ID). I'm not sure what the option should be named. Perhaps
---with-duplicates ?
-
-Add a -0 option to "notmuch search" so that one can safely deal with
-any filename with:
-
-       notmuch search --output=files -0 <terms> | xargs -0 <command>
-
-"notmuch setup" should use realpath() before replacing the
-configuration file. The ensures that the final target file of any
-intermediate symbolic links is what is actually replaced, (rather than
-any symbolic link).
-
-Replace "notmuch reply" with "notmuch compose --reply <search-terms>".
-This would enable a plain "notmuch compose" to be used to construct an
-initial message, (which would then have the properly configured name
-and email address in the From: line. We could also then easily support
-"notmuch compose --from <something>" to support getting at alternate
-email addresses.
-
-Fix the --format=json option to not imply --entire-thread.
-
-Implement "notmuch search --exclude-threads=<search-terms>" to allow
-for excluding muted threads, (and any other negative, thread-based
-filtering that the user wants to do).
-
-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".) 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.
-
-Add a "--format" option to "notmuch search", (something printf-like
-for selecting what gets printed).
-
-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
-matching 1/N messages.
-
-Add a "-f <filename>" option to select an alternate configuration
-file.
-
-Allow configuration for filename patterns that should be ignored when
-indexing.
-
-Replace the "notmuch part --part=id" command with "notmuch show
---part=id", (David Edmondson wants to rewrite some of "notmuch show" to
-provide more MIME-structure information in its output first).
-
-Replace the "notmuch search-tags" command with "notmuch search
---output=tags".
-
-Fix to avoid this ugly message:
-
-       (process:17197): gmime-CRITICAL **: g_mime_message_get_mime_part: assertion `GMIME_IS_MESSAGE (message)' failed
-       Warning: Not indexing empty mime part.
-
-  This probably means adding a test case to generate that message,
-  filing an upstream bug against GMime, and then silencing the
-  notmuch-generated portion of the warning (so that once GMime is
-  fixed, this is all silent).
-
-Simplify notmuch-reply to simply print the headers (we have the
-original values) rather than calling GMime (which encodes) and adding
-the confusing gmime-filter-headers.c code (which decodes).
-
-notmuch library
----------------
-Add support for custom flag<->tag mappings. In the notmuch
-configuration file this could be
-
-       [maildir]
-       synchronize_flags = R:replied; D*:deleted; S:~unread;
-
-In the library interface this could be implemented with an array of
-structures to define the mapping (flag character, tag name,
-inverse-sense bit (~ above), and tag-when-any-file-flagged
-vs. tag-when-all-files-flagged (* above)).
-
-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
-able to say things like "since:2009-01-1" or "until:2009-01-1" and
-have the other endpoint be implicit. Second we'd like to support
-relative specifications of time such as "since:'2 months ago'". To do
-any of this we're probably going to need to break down an write our
-own parser for the query string rather than using Xapian's QueryParser
-class.
-
-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).
-
-Fix to use the *last* Message-ID header if multiple such headers are
-encountered, (I noticed this is one thing that kept me from seeing the
-same message-ID values as sup).
-
-Add support for configuring "virtual tags" which are a tuple of
-(tag-name, search-specification). The database is responsible for
-ensuring that the virtual tag is always consistent.
-
-Indicate to the user if two files with the same message ID have
-content that is actually different in some interesting way. Perhaps
-notmuch initially sees all changes as interesting, and quickly learns
-from the user which changes are not interesting (such as the very
-common mailing-list footer).
-
-Fix notmuch_query_count_messages to share code with
-notmuch_query_search_messages rather than duplicating code. (And
-consider renaming it as well.)
-
-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.
-
-Add symbol hiding so that we don't risk leaking any private symbols
-into the shared-library interface.
-
-Audit all libnotmuch entry points to ensure that all Xapian calls are
-wrapped in a try/catch block.
-
-Fix the "count" functionality to be exact as Olly explained in IRC:
-
-       ojwb> cworth: if you set the check_at_least parameter to the
-       database size, get_matches_estimated() will be exact
-
-Fix the threading of a message that has a References: header but no
-In-Reply-To: header (see id:"87lixxnxpb.fsf@yoom.home.cworth.org").
-
-Search syntax
--------------
-Implement support for "tag:*" to expand to all tags.
-
-Fix "notmuch search to:" to be less confusing. Many users expect this
-to search for all messages with a To: header, but it instead searches
-for all messages with the word "to". If we don't provide the first
-behavior, perhaps we should exit on an error when a configured prefix
-is provided with no value?
-
-Support "*" in all cases and not just as a special case. That is, "* "
-should also work, as well as "* and tag:inbox".
-
-Implement a syntax for requesting set-theoertic operations on results
-of multiple searches. For example, I would like to do:
-
-       "tag:inbox" SET-SUBTRACT "tag:muted"
-
-    as well as:
-
-       "tag:notmuch and <date-range>" SET-INTERSECT
-       "tag:notmuch and not (tag:merged or tag:postponed)"
-
-    See id:3wdpr282yz2.fsf@testarossa.amd.com for more details on the
-    use cases of the above.
-
-Database changes
-----------------
-Store a reference term for every message-id that appears in
-References. We just started doing this for newly-added documents, but
-at the next convenient database-schema upgrade, we should go back and
-fix old messages to be consistent.
-
-Start indexing the List-Id header, (and re-index this header for
-existing messages at the next database upgrade).
-
-Add support for the user to specify custom headers to be indexed (and
-re-index these for existing messages at the next database upgrade).
-
-Save filenames for files detected as "not an email file" in the
-database. This would allow for two things: 1. Optimizing "notmuch new"
-to not have to look at these files again (since they are potentially
-large so the detection could be potentially slow). 2. A "notmuch
-search" syntax could be added to allow the user to find these files,
-(and perhaps delete them or move them away as appropriate).
-
-Fix filesystem/notmuch-new race condition by not updating database
-mtime for a directory if it is the same as the current mtime.
-
-Test suite
-----------
-Achieve 100% test coverage with the test suite.
-
-General
--------
-Audit everything for dealing with out-of-memory (and drop xutil.c).
-
-Investigate why the notmuch database is slightly larger than the sup
-database for the same corpus of email.
-
-Makefile should print message teaching user about LD_LIBRARY_PATH (or
-similar) if libdir is not set to a directory examined by ldconfig.
index 8d1185953b5ccd50c9032b1220624873857701af..3ff53ec8c60ed6459a9eb36b03bf81e246eca838 100755 (executable)
@@ -17,7 +17,12 @@ import stat
 import email
 
 from notmuch import Database, Query, NotmuchError, STATUS
-from ConfigParser import SafeConfigParser
+try:
+    # python3.x
+    from configparser import SafeConfigParser
+except ImportError:
+    # python2.x
+    from ConfigParser import SafeConfigParser
 from cStringIO import StringIO
 
 PREFIX = re.compile('(\w+):(.*$)')
index 0074ba36b2fc7995cddbd3a81af0e4e8c39b087d..6238b2891e066cf76a9361fc573509a287851124 100644 (file)
@@ -430,7 +430,7 @@ class Database(object):
                removed.
         """
         self._assert_db_is_initialized()
-        return self._remove_message(self._db, filename)
+        return self._remove_message(self._db, _str(filename))
 
     def find_message(self, msgid):
         """Returns a :class:`Message` as identified by its message ID
@@ -543,7 +543,13 @@ class Database(object):
         """ Reads a user's notmuch config and returns his db location
 
         Throws a NotmuchError if it cannot find it"""
-        from ConfigParser import SafeConfigParser
+        try:
+            # python3.x
+            from configparser import SafeConfigParser
+        except ImportError:
+            # python2.x
+            from ConfigParser import SafeConfigParser
+
         config = SafeConfigParser()
         conf_f = os.getenv('NOTMUCH_CONFIG',
                            os.path.expanduser('~/.notmuch-config'))
@@ -919,7 +925,7 @@ class Filenames(object):
     _move_to_next.argtypes = [NotmuchFilenamesP]
     _move_to_next.restype = None
 
-    def next(self):
+    def __next__(self):
         if not self._files_p:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
 
@@ -927,9 +933,10 @@ class Filenames(object):
             self._files_p = None
             raise StopIteration
 
-        file = Filenames._get(self._files_p)
+        file_ = Filenames._get(self._files_p)
         self._move_to_next(self._files_p)
-        return file
+        return file_.decode('utf-8', 'ignore')
+    next = __next__ # python2.x iterator protocol compatibility
 
     def __len__(self):
         """len(:class:`Filenames`) returns the number of contained files
index f7313ec5ae856ccb008fe5ce16461e67e875faed..3f541046ff495bc65cb6f28f50eb5fff2e64b0b3 100644 (file)
@@ -18,10 +18,10 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
 """
 from ctypes import c_char_p
 from notmuch.globals import (nmlib, STATUS, NotmuchError,
-    NotmuchFilenamesP, NotmuchMessageP)
+    NotmuchFilenamesP, NotmuchMessageP, _str, Python3StringMixIn)
 
 
-class Filenames(object):
+class Filenames(Python3StringMixIn):
     """Represents a list of filenames as returned by notmuch
 
     This object contains the Filenames iterator. The main function is
@@ -93,14 +93,11 @@ class Filenames(object):
             raise NotmuchError(STATUS.NOT_INITIALIZED)
 
         while self._valid(self._files):
-            yield Filenames._get(self._files)
+            yield Filenames._get(self._files).decode('utf-8', 'ignore')
             self._move_to_next(self._files)
 
         self._files = None
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-
     def __unicode__(self):
         """Represent Filenames() as newline-separated list of full paths
 
index 54a49b2d3f16895a8a8ce0b9ffc459b58a7cadf2..41384604588d16ef70f5eb47f84f62aca129695f 100644 (file)
@@ -16,7 +16,7 @@ along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
 
 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
 """
-
+import sys
 from ctypes import CDLL, c_char_p, c_int, Structure, POINTER
 
 #-----------------------------------------------------------------------------
@@ -27,6 +27,38 @@ except:
     raise ImportError("Could not find shared 'notmuch' library.")
 
 
+if sys.version_info[0] == 2:
+    class Python3StringMixIn(object):
+        def __str__(self):
+            return unicode(self).encode('utf-8')
+
+
+    def _str(value):
+        """Ensure a nicely utf-8 encoded string to pass to libnotmuch
+
+        C++ code expects strings to be well formatted and
+        unicode strings to have no null bytes."""
+        if not isinstance(value, basestring):
+            raise TypeError("Expected str or unicode, got %s" % type(value))
+        if isinstance(value, unicode):
+            return value.encode('UTF-8')
+        return value
+else:
+    class Python3StringMixIn(object):
+        def __str__(self):
+            return self.__unicode__()
+
+
+    def _str(value):
+        """Ensure a nicely utf-8 encoded string to pass to libnotmuch
+
+        C++ code expects strings to be well formatted and
+        unicode strings to have no null bytes."""
+        if not isinstance(value, str):
+            raise TypeError("Expected str, got %s" % type(value))
+        return value.encode('UTF-8')
+
+
 class Enum(object):
     """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc..."""
     def __init__(self, names):
@@ -51,7 +83,7 @@ class Status(Enum):
         """Get a (unicode) string representation of a notmuch_status_t value."""
         # define strings for custom error messages
         if status == STATUS.NOT_INITIALIZED:
-            return u"Operation on uninitialized object impossible."
+            return "Operation on uninitialized object impossible."
         return unicode(Status._status2str(status))
 
 STATUS = Status(['SUCCESS',
@@ -89,7 +121,7 @@ argument to receive a human readable string"""
 STATUS.__name__ = 'STATUS'
 
 
-class NotmuchError(Exception):
+class NotmuchError(Exception, Python3StringMixIn):
     """Is initiated with a (notmuch.STATUS[, message=None]). It will not
     return an instance of the class NotmuchError, but a derived instance
     of a more specific Error Message, e.g. OutOfMemoryError. Each status
@@ -133,16 +165,13 @@ class NotmuchError(Exception):
         self.status = status
         self.message = message
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-
     def __unicode__(self):
         if self.message is not None:
             return self.message
         elif self.status is not None:
             return STATUS.status2str(self.status)
         else:
-            return u'Unknown error'
+            return 'Unknown error'
 
 
 # List of Subclassed exceptions that correspond to STATUS values and are
@@ -195,18 +224,6 @@ class NotInitializedError(NotmuchError):
     status = STATUS.NOT_INITIALIZED
 
 
-def _str(value):
-    """Ensure a nicely utf-8 encoded string to pass to libnotmuch
-
-    C++ code expects strings to be well formatted and
-    unicode strings to have no null bytes."""
-    if not isinstance(value, basestring):
-        raise TypeError("Expected str or unicode, got %s" % str(type(value)))
-    if isinstance(value, unicode):
-        return value.encode('UTF-8')
-    return value
-
-
 class NotmuchDatabaseS(Structure):
     pass
 NotmuchDatabaseP = POINTER(NotmuchDatabaseS)
index 5540df3e76ad562b545802756e31ed8f8e9afb14..883ed233c7e170fbfcab9b58aca2676f31417920 100644 (file)
@@ -21,7 +21,8 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
 
 from ctypes import c_char_p, c_long, c_uint, c_int
 from datetime import date
-from notmuch.globals import (nmlib, STATUS, NotmuchError, Enum, _str,
+from notmuch.globals import (
+    nmlib, STATUS, NotmuchError, Enum, _str, Python3StringMixIn,
     NotmuchTagsP, NotmuchMessagesP, NotmuchMessageP, NotmuchFilenamesP)
 from notmuch.tag import Tags
 from notmuch.filename import Filenames
@@ -158,7 +159,7 @@ class Messages(object):
     _move_to_next.argtypes = [NotmuchMessagesP]
     _move_to_next.restype = None
 
-    def next(self):
+    def __next__(self):
         if self._msgs is None:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
 
@@ -169,6 +170,7 @@ class Messages(object):
         msg = Message(Messages._get(self._msgs), self)
         self._move_to_next(self._msgs)
         return msg
+    next = __next__ # python2.x iterator protocol compatibility
 
     def __nonzero__(self):
         """
@@ -186,14 +188,17 @@ class Messages(object):
         if self._msgs is not None:
             self._destroy(self._msgs)
 
-    def print_messages(self, format, indent=0, entire_thread=False):
-        """Outputs messages as needed for 'notmuch show' to sys.stdout
+    def format_messages(self, format, indent=0, entire_thread=False):
+        """Formats messages as needed for 'notmuch show'.
 
         :param format: A string of either 'text' or 'json'.
         :param indent: A number indicating the reply depth of these messages.
         :param entire_thread: A bool, indicating whether we want to output
                        whole threads or only the matching messages.
+        :return: a list of lines
         """
+        result = list()
+
         if format.lower() == "text":
             set_start = ""
             set_end = ""
@@ -207,38 +212,61 @@ class Messages(object):
 
         first_set = True
 
-        sys.stdout.write(set_start)
+        result.append(set_start)
 
         # iterate through all toplevel messages in this thread
         for msg in self:
             # if not msg:
             #     break
             if not first_set:
-                sys.stdout.write(set_sep)
+                result.append(set_sep)
             first_set = False
 
-            sys.stdout.write(set_start)
+            result.append(set_start)
             match = msg.is_match()
             next_indent = indent
 
             if (match or entire_thread):
                 if format.lower() == "text":
-                    sys.stdout.write(msg.format_message_as_text(indent))
+                    result.append(msg.format_message_as_text(indent))
                 else:
-                    sys.stdout.write(msg.format_message_as_json(indent))
+                    result.append(msg.format_message_as_json(indent))
                 next_indent = indent + 1
 
             # get replies and print them also out (if there are any)
-            replies = msg.get_replies()
-            if not replies is None:
-                sys.stdout.write(set_sep)
-                replies.print_messages(format, next_indent, entire_thread)
+            replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
+            if replies:
+                result.append(set_sep)
+                result.extend(replies)
+
+            result.append(set_end)
+        result.append(set_end)
+
+        return result
+
+    def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
+        """Outputs messages as needed for 'notmuch show' to a file like object.
+
+        :param format: A string of either 'text' or 'json'.
+        :param handle: A file like object to print to (default is sys.stdout).
+        :param indent: A number indicating the reply depth of these messages.
+        :param entire_thread: A bool, indicating whether we want to output
+                       whole threads or only the matching messages.
+        """
+        handle.write(''.join(self.format_messages(format, indent, entire_thread)))
+
+
+class EmptyMessagesResult(Messages):
+    def __init__(self, parent):
+        self._msgs = None
+        self._parent = parent
 
-            sys.stdout.write(set_end)
-        sys.stdout.write(set_end)
+    def __next__(self):
+        raise StopIteration()
+    next = __next__
 
 
-class Message(object):
+class Message(Python3StringMixIn):
     """Represents a single Email message
 
     Technically, this wraps the underlying *notmuch_message_t*
@@ -336,7 +364,7 @@ class Message(object):
         """
         if self._msg is None:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
-        return Message._get_message_id(self._msg)
+        return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
 
     def get_thread_id(self):
         """Returns the thread ID
@@ -354,7 +382,7 @@ class Message(object):
         if self._msg is None:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
 
-        return Message._get_thread_id(self._msg)
+        return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
 
     def get_replies(self):
         """Gets all direct replies to this message as :class:`Messages`
@@ -368,10 +396,9 @@ class Message(object):
             number of subsequent calls to :meth:`get_replies`). If this message
             was obtained through some non-thread means, (such as by a call to
             :meth:`Query.search_messages`), then this function will return
-            `None`.
+            an empty Messages iterator.
 
-        :returns: :class:`Messages` or `None` if there are no replies to
-            this message.
+        :returns: :class:`Messages`.
         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
                     is not initialized.
         """
@@ -381,7 +408,7 @@ class Message(object):
         msgs_p = Message._get_replies(self._msg)
 
         if not msgs_p:
-            return None
+            return EmptyMessagesResult(self)
 
         return Messages(msgs_p, self)
 
@@ -424,10 +451,10 @@ class Message(object):
             raise NotmuchError(STATUS.NOT_INITIALIZED)
 
         #Returns NULL if any error occurs.
-        header = Message._get_header(self._msg, header)
+        header = Message._get_header(self._msg, _str(header))
         if header == None:
             raise NotmuchError(STATUS.NULL_POINTER)
-        return header.decode('UTF-8', errors='ignore')
+        return header.decode('UTF-8', 'ignore')
 
     def get_filename(self):
         """Returns the file path of the message file
@@ -438,7 +465,7 @@ class Message(object):
         """
         if self._msg is None:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
-        return Message._get_filename(self._msg)
+        return Message._get_filename(self._msg).decode('utf-8', 'ignore')
 
     def get_filenames(self):
         """Get all filenames for the email corresponding to 'message'
@@ -795,9 +822,6 @@ class Message(object):
         """Represent a Message() object by str()"""
         return self.__str__()
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-
     def __unicode__(self):
         format = "%s (%s) (%s)"
         return format % (self.get_header('from'),
index 4881db9fa2cdae0220f0f28398e14b69c334b7c1..71d81dd6bc46bdfd5bb3cffddfbea084b3d7b93b 100644 (file)
@@ -17,10 +17,10 @@ along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
 """
 from ctypes import c_char_p
-from notmuch.globals import nmlib, STATUS, NotmuchError, NotmuchTagsP
+from notmuch.globals import nmlib, STATUS, NotmuchError, NotmuchTagsP, _str, Python3StringMixIn
 
 
-class Tags(object):
+class Tags(Python3StringMixIn):
     """Represents a list of notmuch tags
 
     This object provides an iterator over a list of notmuch tags (which
@@ -89,7 +89,7 @@ class Tags(object):
     _move_to_next.argtypes = [NotmuchTagsP]
     _move_to_next.restype = None
 
-    def next(self):
+    def __next__(self):
         if self._tags is None:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
         if not self._valid(self._tags):
@@ -98,6 +98,7 @@ class Tags(object):
         tag = Tags._get(self._tags).decode('UTF-8')
         self._move_to_next(self._tags)
         return tag
+    next = __next__ # python2.x iterator protocol compatibility
 
     def __nonzero__(self):
         """Implement bool(Tags) check that can be repeatedly used
@@ -110,9 +111,6 @@ class Tags(object):
             left."""
         return self._valid(self._tags) > 0
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-
     def __unicode__(self):
         """string representation of :class:`Tags`: a space separated list of tags
 
index 594fa5228ecbc7dec1b32554fbdea1f956650c62..104710c4374cd30cf3e5ff755f09fa8297aca14b 100644 (file)
@@ -20,13 +20,13 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
 from ctypes import c_char_p, c_long, c_int
 from notmuch.globals import (nmlib, STATUS,
     NotmuchError, NotmuchThreadP, NotmuchThreadsP, NotmuchMessagesP,
-    NotmuchTagsP,)
+    NotmuchTagsP, Python3StringMixIn)
 from notmuch.message import Messages
 from notmuch.tag import Tags
 from datetime import date
 
 
-class Threads(object):
+class Threads(Python3StringMixIn):
     """Represents a list of notmuch threads
 
     This object provides an iterator over a list of notmuch threads
@@ -116,7 +116,7 @@ class Threads(object):
     _move_to_next.argtypes = [NotmuchThreadsP]
     _move_to_next.restype = None
 
-    def next(self):
+    def __next__(self):
         if self._threads is None:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
 
@@ -127,6 +127,7 @@ class Threads(object):
         thread = Thread(Threads._get(self._threads), self)
         self._move_to_next(self._threads)
         return thread
+    next = __next__ # python2.x iterator protocol compatibility
 
     def __len__(self):
         """len(:class:`Threads`) returns the number of contained Threads
@@ -245,7 +246,7 @@ class Thread(object):
         """
         if self._thread is None:
             raise NotmuchError(STATUS.NOT_INITIALIZED)
-        return Thread._get_thread_id(self._thread)
+        return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore')
 
     _get_total_messages = nmlib.notmuch_thread_get_total_messages
     _get_total_messages.argtypes = [NotmuchThreadP]
@@ -325,7 +326,7 @@ class Thread(object):
         authors = Thread._get_authors(self._thread)
         if authors is None:
             return None
-        return authors.decode('UTF-8', errors='ignore')
+        return authors.decode('UTF-8', 'ignore')
 
     def get_subject(self):
         """Returns the Subject of 'thread'
@@ -338,7 +339,7 @@ class Thread(object):
         subject = Thread._get_subject(self._thread)
         if subject is None:
             return None
-        return subject.decode('UTF-8', errors='ignore')
+        return subject.decode('UTF-8', 'ignore')
 
     def get_newest_date(self):
         """Returns time_t of the newest message date
@@ -391,9 +392,6 @@ class Thread(object):
             raise NotmuchError(STATUS.NULL_POINTER)
         return Tags(tags_p, self)
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-
     def __unicode__(self):
         frm = "thread:%s %12s [%d/%d] %s; %s (%s)"
 
index 286fd1962187b6351036d54d547e69dd9c0b1016..2e58dab1d6a6cc3a6193031f2410b082e385b9eb 100644 (file)
@@ -7,7 +7,7 @@ from distutils.core import setup
 # get the notmuch version number without importing the notmuch module
 version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                             'notmuch', 'version.py')
-execfile(version_file)
+exec(compile(open(version_file).read(), version_file, 'exec'))
 assert __VERSION__, 'Failed to read the notmuch binding version number'
 
 setup(name='notmuch',
index 504eb7157cc281e8c9c5483ca42637f0740476b4..13f16cd3fa30f29041f4d9e6aaab1cc19aee33d9 100644 (file)
@@ -12,3 +12,5 @@ endif
 ifneq ($(HAVE_STRCASESTR),1)
 notmuch_compat_srcs += $(dir)/strcasestr.c
 endif
+
+SRCS := $(SRCS) $(notmuch_compat_srcs)
index 7767fe84a68580dbd2d2953f99645eda81403cac..b2e27368cf0690701c81b692fc0f9dce4c7216f4 100644 (file)
@@ -46,6 +46,14 @@ getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp);
 char* strcasestr(const char *haystack, const char *needle);
 #endif /* !HAVE_STRCASESTR */
 
+/* Silence gcc warnings about unused results.  These warnings exist
+ * for a reason; any use of this needs to be justified. */
+#ifdef __GNUC__
+#define IGNORE_RESULT(x) ({ __typeof__(x) __z = (x); (void)(__z = __z); })
+#else /* !__GNUC__ */
+#define IGNORE_RESULT(x) x
+#endif /* __GNUC__ */
+
 #ifdef __cplusplus
 }
 #endif
index e90b76fa187f3b05893d90f085b595875fb79b35..8b85b9d9f6b11c6253b94f5f8db5ef6547b50070 100755 (executable)
--- a/configure
+++ b/configure
@@ -289,10 +289,10 @@ if [ "$have_gmime" = "0" ]; then
 fi
 
 # GMime already depends on Glib >= 2.12, but we use at least one Glib
-# function that only exists as of 2.14, (g_hash_table_get_keys)
-printf "Checking for Glib development files (>= 2.14)... "
+# function that only exists as of 2.22, (g_array_unref)
+printf "Checking for Glib development files (>= 2.22)... "
 have_glib=0
-if pkg-config --exists 'glib-2.0 >= 2.14'; then
+if pkg-config --exists 'glib-2.0 >= 2.22'; then
     printf "Yes.\n"
     have_glib=1
     glib_cflags=$(pkg-config --cflags glib-2.0)
@@ -416,7 +416,7 @@ EOF
        echo "  http://spruce.sourceforge.net/gmime/"
     fi
     if [ $have_glib -eq 0 ]; then
-       echo "  Glib library >= 2.14 (including development files such as headers)"
+       echo "  Glib library >= 2.22 (including development files such as headers)"
        echo "  http://ftp.gnome.org/pub/gnome/sources/glib/"
     fi
     if [ $have_talloc -eq 0 ]; then
index fff498d20f55896121a232da5a46a877e6f04005..86e891d4aefc5ad6a593b68f81981dfa85bbcf45 100644 (file)
@@ -1,3 +1,3 @@
 usr/bin
-usr/share/man/man1
+usr/share/man
 etc/bash_completion.d
diff --git a/devel/RELEASING b/devel/RELEASING
new file mode 100644 (file)
index 0000000..88dab04
--- /dev/null
@@ -0,0 +1,113 @@
+Here are the steps to follow to create a new notmuch release.
+
+These steps assume that a process (not described here) has already
+been followed to determine the features and bug fixes to be included
+in a release, and that adequate testing by the community has already
+been performed. The little bit of testing performed here is a safety
+check, and not a substitute for wider testing.
+
+OK, so the code to be released is present and committed to your git
+repository. From here, there are just a few steps to release:
+
+1) 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, please add them, (and ask the
+       authors of the commits to update NEWS in the future).
+
+2) Verify that the library version in lib/Makefile.local is correct
+
+       See the instructions there for how to increment it.
+
+       The version should have been updated with any commits that
+       added API _in a non-upwardly compatible_ way, but do check
+       that that is the case. The command below can be useful for
+       inspecting header-file changes since the last release X.Y:
+
+               git diff X.Y..HEAD -- lib/notmuch.h
+
+       Commit this change, if any.
+
+3) Update the debian/libnotmuchX.symbols file
+
+       If the library version changed at all (step 2) it probably
+       means that symbols have changed/been added, in which case the
+       debian symbols file also needs to be updated:
+
+              dpkg-buildpackage -uc -us
+              dpkg-gensymbols -plibnotmuchX | patch -p0
+
+       Carefully review the changes to debian/libnotmuch1.symbols to
+       make sure there are no unexpected changes.  Remove any debian
+       versions from symbols.
+
+       Commit this change, if any.
+
+4) Upgrade the version in the file "version"
+
+       The scheme for the release number is as follows:
+
+       A major milestone in usability causes an increase in the major
+       number, yielding a two-component version with a minor number
+       of 0, (such as "1.0" or "2.0").
+
+       Otherwise, releases with changes in features cause an increase
+       in the minor number, yielding a two-component version, (such
+       as "1.1" or "1.2").
+
+       Finally, releases that do not change "features" but are merely
+       bug fixes either increase the micro number or add it (starting
+       at ".1" if not present). So a bug-fix release from "1.0" would
+       be "1.0.1" and a subsequent bug-fix release would be "1.0.2"
+       etc.
+
+       When you are happy with the file 'version', run
+
+            make update-versions
+
+       to propagate the version to the other places needed.
+
+       Commit these changes.
+
+5) Create an entry for the new release in debian/changelog
+
+       The syntax of this file is tightly restricted, but the
+       available emacs mode (see the dpkg-dev-el package) helps.
+       The entries here will be the Debian-relevant single-line
+       description of changes from the NEWS entry. And the version
+       must match the version in the next step.
+
+       Commit this change.
+
+       XXX: It would be great if this step were automated as part of
+       release, (taking entries from NEWS and the version from the
+       version file, and creating a new commit, etc.)
+
+6) Run "make release" which will perform the following steps.
+
+   Note: in order to really upload anything, set the make variable
+   REALLY_UPLOAD=yes
+
+       * Ensure that the version consists only of digits and periods
+       * Ensure that version and debian/changelog have the same version
+       * Verify that the source tree is clean
+       * Compile the current notmuch code (aborting release if it fails)
+       * Run the notmuch test suite (aborting release if it fails)
+       * Check that no release exists with the current version
+       * Make a signed tag
+       * Generate a tar file from this tag
+       * Generate a .sha1 sum file for the tar file and GPG sign it.
+       * Commit a (delta for a) copy of the tar file using pristine-tar
+       * Tag for the debian version
+       * if REALLY_UPLOAD=yes
+         - push the signed tag to the origin
+           XXX FIXME push debian tag
+         - scp tarball to web site
+       * Provide some text for the release announcement (see below).
+
+7) Send a message to notmuch@notmuchmail.org to announce the release.
+
+       Use the text provided from "make release" above, (if for some
+       reason you lose this message, "make release-message" prints
+       it again for you.
diff --git a/devel/TODO b/devel/TODO
new file mode 100644 (file)
index 0000000..4dda6f4
--- /dev/null
@@ -0,0 +1,285 @@
+Fix the things that are causing the most pain to new users
+----------------------------------------------------------
+1. A new import is tagging all messages as "inbox" -- total pain
+
+Emacs interface (notmuch.el)
+----------------------------
+Add notmuch-bcc and notmuch-cc for setting default Bcc and Cc values,
+(should affect the message-setup-hook).
+
+Switch the notmuch-search view to use "notmuch search --format=json"
+to fix large classes of bugs regarding poorly-escaped output and lame
+regular expressions. (The most recently found, unfixed example is the
+sender's name containing ';' which causes emacs to drop a search
+result.) This may require removing the outer array from the current
+"notmuch search --format=json" results.
+
+Fix '*' to work by simply calling '+' or '-' on a region consisting of
+the entire buffer, (this would avoid one race condition---while still
+leaving other race conditions---but could also potentially make '*' a
+very expensive operation).
+
+Add a global keybinding table for notmuch, and then view-specific
+tables that add to it.
+       
+Add a '|' binding from the search view.
+
+Add support for choosing from one of the user's configured email
+addresses for the From line.
+
+Make 'notmuch-show-pipe-message have a private history.
+
+Add support for a delete keybinding that adds a "deleted" tag to the
+current message/thread and make searches not return deleted messages
+by default, (unless the user asks explicitly for deleted messages in
+the search query).
+
+Add keybindings for next/previous thread.
+
+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).
+
+Make '=' count from the end rather than from the beginning if more
+than half-way through the buffer.
+
+Fix to automatically wrap long headers (for RFC compliance) before
+sending. This should probably just be fixed in message-mode itself,
+(but perhaps we can have a notmuch-message-mode that layers this on
+top).
+
+Stop hiding the headers so much in the thread-view mode.
+
+Allow opening a message in thread-view mode by clicking on either
+line.
+
+Automatically open a message when navigating to it with N or P.
+
+Change 'a' command in thread-view mode to only archive open messages.
+
+Add a binding to open all closed messages.
+
+Change the 'a'rchive command in the thread view to only archive open
+messages.
+
+Completion
+----------
+Fix bash completion to complete multiple search options (both --first
+and *then* --max-threads), and also complete value for --sort=
+(oldest-first or newest-first).
+
+notmuch command-line tool
+-------------------------
+Add support to "notmuch search" and "notmuch show" to allow for
+listing of duplicate messages, (distinct filenames with the same
+Message-ID). I'm not sure what the option should be named. Perhaps
+--with-duplicates ?
+
+Add a -0 option to "notmuch search" so that one can safely deal with
+any filename with:
+
+       notmuch search --output=files -0 <terms> | xargs -0 <command>
+
+"notmuch setup" should use realpath() before replacing the
+configuration file. The ensures that the final target file of any
+intermediate symbolic links is what is actually replaced, (rather than
+any symbolic link).
+
+Replace "notmuch reply" with "notmuch compose --reply <search-terms>".
+This would enable a plain "notmuch compose" to be used to construct an
+initial message, (which would then have the properly configured name
+and email address in the From: line. We could also then easily support
+"notmuch compose --from <something>" to support getting at alternate
+email addresses.
+
+Fix the --format=json option to not imply --entire-thread.
+
+Implement "notmuch search --exclude-threads=<search-terms>" to allow
+for excluding muted threads, (and any other negative, thread-based
+filtering that the user wants to do).
+
+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".) 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.
+
+Add a "--format" option to "notmuch search", (something printf-like
+for selecting what gets printed).
+
+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
+matching 1/N messages.
+
+Add a "-f <filename>" option to select an alternate configuration
+file.
+
+Allow configuration for filename patterns that should be ignored when
+indexing.
+
+Replace the "notmuch part --part=id" command with "notmuch show
+--part=id", (David Edmondson wants to rewrite some of "notmuch show" to
+provide more MIME-structure information in its output first).
+
+Replace the "notmuch search-tags" command with "notmuch search
+--output=tags".
+
+Fix to avoid this ugly message:
+
+       (process:17197): gmime-CRITICAL **: g_mime_message_get_mime_part: assertion `GMIME_IS_MESSAGE (message)' failed
+       Warning: Not indexing empty mime part.
+
+  This probably means adding a test case to generate that message,
+  filing an upstream bug against GMime, and then silencing the
+  notmuch-generated portion of the warning (so that once GMime is
+  fixed, this is all silent).
+
+Simplify notmuch-reply to simply print the headers (we have the
+original values) rather than calling GMime (which encodes) and adding
+the confusing gmime-filter-headers.c code (which decodes).
+
+notmuch library
+---------------
+Add support for custom flag<->tag mappings. In the notmuch
+configuration file this could be
+
+       [maildir]
+       synchronize_flags = R:replied; D*:deleted; S:~unread;
+
+In the library interface this could be implemented with an array of
+structures to define the mapping (flag character, tag name,
+inverse-sense bit (~ above), and tag-when-any-file-flagged
+vs. tag-when-all-files-flagged (* above)).
+
+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
+able to say things like "since:2009-01-1" or "until:2009-01-1" and
+have the other endpoint be implicit. Second we'd like to support
+relative specifications of time such as "since:'2 months ago'". To do
+any of this we're probably going to need to break down an write our
+own parser for the query string rather than using Xapian's QueryParser
+class.
+
+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).
+
+Fix to use the *last* Message-ID header if multiple such headers are
+encountered, (I noticed this is one thing that kept me from seeing the
+same message-ID values as sup).
+
+Add support for configuring "virtual tags" which are a tuple of
+(tag-name, search-specification). The database is responsible for
+ensuring that the virtual tag is always consistent.
+
+Indicate to the user if two files with the same message ID have
+content that is actually different in some interesting way. Perhaps
+notmuch initially sees all changes as interesting, and quickly learns
+from the user which changes are not interesting (such as the very
+common mailing-list footer).
+
+Fix notmuch_query_count_messages to share code with
+notmuch_query_search_messages rather than duplicating code. (And
+consider renaming it as well.)
+
+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.
+
+Add symbol hiding so that we don't risk leaking any private symbols
+into the shared-library interface.
+
+Audit all libnotmuch entry points to ensure that all Xapian calls are
+wrapped in a try/catch block.
+
+Fix the "count" functionality to be exact as Olly explained in IRC:
+
+       ojwb> cworth: if you set the check_at_least parameter to the
+       database size, get_matches_estimated() will be exact
+
+Fix the threading of a message that has a References: header but no
+In-Reply-To: header (see id:"87lixxnxpb.fsf@yoom.home.cworth.org").
+
+Search syntax
+-------------
+Implement support for "tag:*" to expand to all tags.
+
+Fix "notmuch search to:" to be less confusing. Many users expect this
+to search for all messages with a To: header, but it instead searches
+for all messages with the word "to". If we don't provide the first
+behavior, perhaps we should exit on an error when a configured prefix
+is provided with no value?
+
+Support "*" in all cases and not just as a special case. That is, "* "
+should also work, as well as "* and tag:inbox".
+
+Implement a syntax for requesting set-theoertic operations on results
+of multiple searches. For example, I would like to do:
+
+       "tag:inbox" SET-SUBTRACT "tag:muted"
+
+    as well as:
+
+       "tag:notmuch and <date-range>" SET-INTERSECT
+       "tag:notmuch and not (tag:merged or tag:postponed)"
+
+    See id:3wdpr282yz2.fsf@testarossa.amd.com for more details on the
+    use cases of the above.
+
+Database changes
+----------------
+Store a reference term for every message-id that appears in
+References. We just started doing this for newly-added documents, but
+at the next convenient database-schema upgrade, we should go back and
+fix old messages to be consistent.
+
+Start indexing the List-Id header, (and re-index this header for
+existing messages at the next database upgrade).
+
+Add support for the user to specify custom headers to be indexed (and
+re-index these for existing messages at the next database upgrade).
+
+Save filenames for files detected as "not an email file" in the
+database. This would allow for two things: 1. Optimizing "notmuch new"
+to not have to look at these files again (since they are potentially
+large so the detection could be potentially slow). 2. A "notmuch
+search" syntax could be added to allow the user to find these files,
+(and perhaps delete them or move them away as appropriate).
+
+Fix filesystem/notmuch-new race condition by not updating database
+mtime for a directory if it is the same as the current mtime.
+
+Test suite
+----------
+Achieve 100% test coverage with the test suite.
+
+General
+-------
+Audit everything for dealing with out-of-memory (and drop xutil.c).
+
+Investigate why the notmuch database is slightly larger than the sup
+database for the same corpus of email.
+
+Makefile should print message teaching user about LD_LIBRARY_PATH (or
+similar) if libdir is not set to a directory examined by ldconfig.
diff --git a/devel/uncrustify.cfg b/devel/uncrustify.cfg
new file mode 100644 (file)
index 0000000..d8075ba
--- /dev/null
@@ -0,0 +1,112 @@
+#
+# uncrustify config file for the linux kernel
+#
+# $Id: linux-indent.cfg 488 2006-09-09 12:44:38Z bengardner $
+# Taken from the uncrustify distribution under license (GPL2+)
+#
+# sample usage:
+#        uncrustify --replace -c uncrustify.cfg foo.c
+#
+#
+
+indent_with_tabs       = 2             # 1=indent to level only, 2=indent with tabs
+align_with_tabs                = TRUE          # use tabs to align
+align_on_tabstop       = TRUE          # align on tabstops
+input_tab_size         = 8             # original tab size
+output_tab_size                = 8             # new tab size
+indent_columns         = 4
+
+indent_label           = -2            # pos: absolute col, neg: relative column
+
+#
+# inter-symbol newlines
+#
+
+nl_enum_brace          = remove        # "enum {" vs "enum \n {"
+nl_union_brace         = remove        # "union {" vs "union \n {"
+nl_struct_brace                = remove        # "struct {" vs "struct \n {"
+nl_do_brace             = remove       # "do {" vs "do \n {"
+nl_if_brace             = remove       # "if () {" vs "if () \n {"
+nl_for_brace            = remove       # "for () {" vs "for () \n {"
+nl_else_brace           = remove       # "else {" vs "else \n {"
+nl_while_brace          = remove       # "while () {" vs "while () \n {"
+nl_switch_brace         = remove       # "switch () {" vs "switch () \n {"
+nl_brace_while         = remove        # "} while" vs "} \n while" - cuddle while
+nl_brace_else          = remove        # "} else" vs "} \n else" - cuddle else
+nl_func_var_def_blk    = 1
+nl_fcall_brace         = remove        # "list_for_each() {" vs "list_for_each()\n{"
+nl_fdef_brace          = force         # "int foo() {" vs "int foo()\n{"
+# nl_after_return              = TRUE;
+# nl_before_case       = 1
+
+# Add or remove newline between return type and function name in definition
+nl_func_type_name      = force
+nl_enum_leave_one_liners = True
+nl_enum_brace = Remove
+nl_after_struct = 0
+#
+# Source code modifications
+#
+
+# mod_paren_on_return  = remove        # "return 1;" vs "return (1);"
+# mod_full_brace_if    = remove        # "if (a) a--;" vs "if (a) { a--; }"
+# mod_full_brace_for   = remove        # "for () a--;" vs "for () { a--; }"
+# mod_full_brace_do    = remove        # "do a--; while ();" vs "do { a--; } while ();"
+# mod_full_brace_while = remove        # "while (a) a--;" vs "while (a) { a--; }"
+
+#
+# Extra types used in notmuch source.
+# (add more on demand)
+
+type GMimeObject mime_node_t
+
+#
+# inter-character spacing options
+#
+
+sp_before_ptr_star     = force
+sp_between_ptr_star    = remove
+sp_after_ptr_star      = remove
+sp_not                 = force
+sp_pp_concat           = ignore        # XXX 'remove' drops leading space also
+sp_pp_stringify                = remove
+
+# sp _return_paren     = force         # "return (1);" vs "return(1);"
+sp_sizeof_paren                = force         # "sizeof (int)" vs "sizeof(int)"
+sp_before_sparen       = force         # "if (" vs "if("
+sp_after_sparen                = force         # "if () {" vs "if (){"
+sp_sparen_brace                = force
+sp_after_cast          = force         # "(int) a" vs "(int)a"
+sp_inside_braces       = add           # "{ 1 }" vs "{1}"
+sp_inside_braces_struct        = add           # "{ 1 }" vs "{1}"
+sp_inside_braces_enum  = add           # "{ 1 }" vs "{1}"
+sp_assign              = force
+sp_arith               = force
+sp_bool                        = add
+sp_compare             = add
+sp_assign              = add
+sp_after_comma         = add
+sp_func_def_paren      = force         # "int foo (){" vs "int foo(){"
+sp_func_call_paren     = force         # "foo (" vs "foo("
+sp_func_proto_paren    = force         # "int foo ();" vs "int foo();"
+sp_brace_else          = force         # "} else" vs "}else"
+sp_else_brace          = force         # "else {" vs "else{"
+#
+# Aligning stuff
+#
+
+align_enum_equ_span    = 4             # '=' in enum definition
+# align_nl_cont                = TRUE
+# align_var_def_span   = 2
+# align_var_def_inline = TRUE
+# align_var_def_star   = FALSE
+# align_var_def_colon  = TRUE
+# align_assign_span    = 1
+align_struct_init_span = 0             # align stuff in a structure init '= { }'
+align_right_cmt_span   = 8             # align comments span this much in func
+# align_pp_define_span = 8;
+# align_pp_define_gap  = 4;
+
+# cmt_star_cont                = FALSE
+
+# indent_brace         = 0
index 0c58b82457db20174006f32765bf00a645eaec18..4fee0e89c3dc7e5ac0c17db11c2aa4d118bc7f70 100644 (file)
@@ -13,7 +13,8 @@ emacs_sources := \
        $(dir)/notmuch-maildir-fcc.el \
        $(dir)/notmuch-message.el \
        $(dir)/notmuch-crypto.el \
-       $(dir)/coolj.el
+       $(dir)/coolj.el \
+       $(dir)/notmuch-print.el
 
 emacs_images := \
        $(srcdir)/$(dir)/notmuch-logo.png
index 8eba7a0b77681f1f03c597840ef2276eda554adc..2bf762ba9b1a0ae3a3970cf2ac2dfdc4ef4071f1 100644 (file)
@@ -28,7 +28,8 @@
 single argument and output a list of possible matches, one per
 line."
   :type 'string
-  :group 'notmuch)
+  :group 'notmuch-send
+  :group 'notmuch-external)
 
 (defvar notmuch-address-message-alist-member
   '("^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):"
@@ -37,9 +38,9 @@ line."
 (defvar notmuch-address-history nil)
 
 (defun notmuch-address-message-insinuate ()
-  (if (not (memq notmuch-address-message-alist-member message-completion-alist))
-      (setq message-completion-alist
-           (push notmuch-address-message-alist-member message-completion-alist))))
+  (unless (memq notmuch-address-message-alist-member message-completion-alist)
+    (setq message-completion-alist
+         (push notmuch-address-message-alist-member message-completion-alist))))
 
 (defun notmuch-address-options (original)
   (process-lines notmuch-address-command original))
index ac30098734283e76c88eb6cb97df5badd04cf3e1..80ac350e6c19f4e871f063280d1c742e02bc0be2 100644 (file)
@@ -34,38 +34,44 @@ The effect of setting this variable can be seen temporarily by
 providing a prefix when viewing a signed or encrypted message, or
 by providing a prefix when reloading the message in notmuch-show
 mode."
-  :group 'notmuch
-  :type 'boolean)
+  :type 'boolean
+  :group 'notmuch-crypto)
 
 (defface notmuch-crypto-part-header
   '((t (:foreground "blue")))
   "Face used for crypto parts headers."
-  :group 'notmuch)
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
 
 (defface notmuch-crypto-signature-good
   '((t (:background "green" :foreground "black")))
   "Face used for good signatures."
-  :group 'notmuch)
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
 
 (defface notmuch-crypto-signature-good-key
   '((t (:background "orange" :foreground "black")))
   "Face used for good signatures."
-  :group 'notmuch)
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
 
 (defface notmuch-crypto-signature-bad
   '((t (:background "red" :foreground "black")))
   "Face used for bad signatures."
-  :group 'notmuch)
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
 
 (defface notmuch-crypto-signature-unknown
   '((t (:background "red" :foreground "black")))
   "Face used for signatures of unknown status."
-  :group 'notmuch)
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
 
 (defface notmuch-crypto-decryption
   '((t (:background "purple" :foreground "black")))
   "Face used for encryption/decryption status messages."
-  :group 'notmuch)
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
 
 (define-button-type 'notmuch-crypto-status-button-type
   'action (lambda (button) (message (button-get button 'help-echo)))
index 333d4c1ea633749cabb7dcf0e31f78241b1eadd2..d17a30f91e0c830dc3e394c97ff48b666401936d 100644 (file)
 (declare-function notmuch-search "notmuch" (query &optional oldest-first target-thread target-line continuation))
 (declare-function notmuch-poll "notmuch" ())
 
-(defvar notmuch-hello-search-bar-marker nil
-  "The position of the search bar within the notmuch-hello buffer.")
-
-(defcustom notmuch-recent-searches-max 10
-  "The number of recent searches to store and display."
+(defcustom notmuch-hello-recent-searches-max 10
+  "The number of recent searches to display."
   :type 'integer
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defcustom notmuch-show-empty-saved-searches nil
   "Should saved searches with no messages be listed?"
   :type 'boolean
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defun notmuch-sort-saved-searches (alist)
   "Generate an alphabetically sorted saved searches alist."
@@ -60,7 +57,7 @@ alist to be used."
                 (const :tag "Sort alphabetically" notmuch-sort-saved-searches)
                 (function :tag "Custom sort function"
                           :value notmuch-sort-saved-searches))
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defvar notmuch-hello-indent 4
   "How much to indent non-headers.")
@@ -68,12 +65,12 @@ alist to be used."
 (defcustom notmuch-show-logo t
   "Should the notmuch logo be shown?"
   :type 'boolean
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defcustom notmuch-show-all-tags-list nil
   "Should all tags be shown in the notmuch-hello view?"
   :type 'boolean
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defcustom notmuch-hello-tag-list-make-query nil
   "Function or string to generate queries for the all tags list.
@@ -89,12 +86,12 @@ should return a filter for that tag, or nil to hide the tag."
                 (string :tag "Custom filter"
                         :value "tag:unread")
                 (function :tag "Custom filter function"))
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defcustom notmuch-hello-hide-tags nil
   "List of tags to be hidden in the \"all tags\"-section."
   :type '(repeat string)
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defface notmuch-hello-logo-background
   '((((class color)
@@ -104,7 +101,8 @@ should return a filter for that tag, or nil to hide the tag."
       (background light))
      (:background "white")))
   "Background colour for the notmuch logo."
-  :group 'notmuch)
+  :group 'notmuch-hello
+  :group 'notmuch-faces)
 
 (defcustom notmuch-column-control t
   "Controls the number of columns for saved searches/tags in notmuch view.
@@ -126,11 +124,11 @@ So:
   30.
 - if you don't want to worry about all of this nonsense, leave
   this set to `t'."
-  :group 'notmuch
   :type '(choice
          (const :tag "Automatically calculated" t)
          (integer :tag "Number of characters")
-         (float :tag "Fraction of window")))
+         (float :tag "Fraction of window"))
+  :group 'notmuch-hello)
 
 (defcustom notmuch-hello-thousands-separator " "
   "The string used as a thousands separator.
@@ -138,32 +136,24 @@ So:
 Typically \",\" in the US and UK and \".\" or \" \" in Europe.
 The latter is recommended in the SI/ISO 31-0 standard and by the
 International Bureau of Weights and Measures."
-  :group 'notmuch
-  :type 'string)
+  :type 'string
+  :group 'notmuch-hello)
 
 (defcustom notmuch-hello-mode-hook nil
   "Functions called after entering `notmuch-hello-mode'."
-  :group 'notmuch
-  :type 'hook)
+  :type 'hook
+  :group 'notmuch-hello
+  :group 'notmuch-hooks)
 
 (defcustom notmuch-hello-refresh-hook nil
   "Functions called after updating a `notmuch-hello' buffer."
   :type 'hook
-  :group 'notmuch)
+  :group 'notmuch-hello
+  :group 'notmuch-hooks)
 
 (defvar notmuch-hello-url "http://notmuchmail.org"
   "The `notmuch' web site.")
 
-(defvar notmuch-hello-recent-searches nil)
-
-(defun notmuch-hello-remember-search (search)
-  (setq notmuch-hello-recent-searches
-       (delete search notmuch-hello-recent-searches))
-  (push search notmuch-hello-recent-searches)
-  (if (> (length notmuch-hello-recent-searches)
-        notmuch-recent-searches-max)
-      (setq notmuch-hello-recent-searches (butlast notmuch-hello-recent-searches))))
-
 (defun notmuch-hello-nice-number (n)
   (let (result)
     (while (> n 0)
@@ -182,10 +172,14 @@ International Bureau of Weights and Measures."
       (match-string 1 search)
     search))
 
-(defun notmuch-hello-search (search)
-  (let ((search (notmuch-hello-trim search)))
-    (notmuch-hello-remember-search search)
-    (notmuch-search search notmuch-search-oldest-first nil nil #'notmuch-hello-search-continuation)))
+(defun notmuch-hello-search (&optional search)
+  (interactive)
+  (unless (null search)
+    (setq search (notmuch-hello-trim search))
+    (let ((history-delete-duplicates t))
+      (add-to-history 'notmuch-search-history search)))
+  (notmuch-search search notmuch-search-oldest-first nil nil
+                 #'notmuch-hello-search-continuation))
 
 (defun notmuch-hello-add-saved-search (widget)
   (interactive)
@@ -299,15 +293,17 @@ should be. Returns a cons cell `(tags-per-line width)'."
                               :notify #'notmuch-hello-widget-search
                               :notmuch-search-terms query
                               formatted-name)
-               ;; Insert enough space to consume the rest of the
-               ;; column.  Because the button for the name is `(1+
-               ;; (length name))' long (due to the trailing space) we
-               ;; can just insert `(- widest (length name))' spaces -
-               ;; the column separator is included in the button if
-               ;; `(equal widest (length name)'.
-               (widget-insert (make-string (max 1
-                                                (- widest (length name)))
-                                           ? ))))
+               (unless (eq (% count tags-per-line) (1- tags-per-line))
+                 ;; If this is not the last tag on the line, insert
+                 ;; enough space to consume the rest of the column.
+                 ;; Because the button for the name is `(1+ (length
+                 ;; name))' long (due to the trailing space) we can
+                 ;; just insert `(- widest (length name))' spaces - the
+                 ;; column separator is included in the button if
+                 ;; `(equal widest (length name)'.
+                 (widget-insert (make-string (max 1
+                                                  (- widest (length name)))
+                                             ? )))))
            (setq count (1+ count))
            (if (eq (% count tags-per-line) 0)
                (widget-insert "\n")))
@@ -315,15 +311,10 @@ should be. Returns a cons cell `(tags-per-line width)'."
 
     ;; If the last line was not full (and hence did not include a
     ;; carriage return), insert one now.
-    (if (not (eq (% count tags-per-line) 0))
-       (widget-insert "\n"))
+    (unless (eq (% count tags-per-line) 0)
+      (widget-insert "\n"))
     found-target-pos))
 
-(defun notmuch-hello-goto-search ()
-  "Put point inside the `search' widget."
-  (interactive)
-  (goto-char notmuch-hello-search-bar-marker))
-
 (defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png")))
 
 (defun notmuch-hello-search-continuation()
@@ -353,7 +344,7 @@ should be. Returns a cons cell `(tags-per-line width)'."
     (define-key map "G" 'notmuch-hello-poll-and-update)
     (define-key map (kbd "<C-tab>") 'widget-backward)
     (define-key map "m" 'notmuch-mua-new-mail)
-    (define-key map "s" 'notmuch-hello-goto-search)
+    (define-key map "s" 'notmuch-hello-search)
     map)
   "Keymap for \"notmuch hello\" buffers.")
 (fset 'notmuch-hello-mode-map notmuch-hello-mode-map)
@@ -397,9 +388,9 @@ Complete list of currently available key bindings:
   "Run notmuch and display saved searches, known tags, etc."
   (interactive)
 
-  ; Jump through a hoop to get this value from the deprecated variable
-  ; name (`notmuch-folders') or from the default value.
-  (if (not notmuch-saved-searches)
+  ;; Jump through a hoop to get this value from the deprecated variable
+  ;; name (`notmuch-folders') or from the default value.
+  (unless notmuch-saved-searches
     (setq notmuch-saved-searches (notmuch-saved-searches)))
 
   (if no-display
@@ -466,7 +457,8 @@ Complete list of currently available key bindings:
       (widget-insert " messages.\n"))
 
     (let ((found-target-pos nil)
-         (final-target-pos nil))
+         (final-target-pos nil)
+         (default-pos))
       (let* ((saved-alist
              ;; Filter out empty saved searches if required.
              (if notmuch-show-empty-saved-searches
@@ -498,7 +490,7 @@ Complete list of currently available key bindings:
            (indent-rigidly start (point) notmuch-hello-indent)))
 
        (widget-insert "\nSearch: ")
-       (setq notmuch-hello-search-bar-marker (point-marker))
+       (setq default-pos (point-marker))
        (widget-create 'editable-field
                       ;; Leave some space at the start and end of the
                       ;; search boxes.
@@ -506,24 +498,27 @@ Complete list of currently available key bindings:
                                       (length "Search: ")))
                       :action (lambda (widget &rest ignore)
                                 (notmuch-hello-search (widget-value widget))))
-       ;; add an invisible space to make `widget-end-of-line' ignore
-       ;; trailine spaces in the search widget field
-       (widget-insert " ")
+       ;; Add an invisible dot to make `widget-end-of-line' ignore
+       ;; trailing spaces in the search widget field.  A dot is used
+       ;; instead of a space to make `show-trailing-whitespace'
+       ;; happy, i.e. avoid it marking the whole line as trailing
+       ;; spaces.
+       (widget-insert ".")
        (put-text-property (1- (point)) (point) 'invisible t)
        (widget-insert "\n")
 
-       (when notmuch-hello-recent-searches
+       (when notmuch-search-history
          (widget-insert "\nRecent searches: ")
          (widget-create 'push-button
                         :notify (lambda (&rest ignore)
-                                  (setq notmuch-hello-recent-searches nil)
+                                  (setq notmuch-search-history nil)
                                   (notmuch-hello-update))
                         "clear")
          (widget-insert "\n\n")
-         (let ((start (point))
-               (nth 0))
-           (mapc (lambda (search)
-                   (let ((widget-symbol (intern (format "notmuch-hello-search-%d" nth))))
+         (let ((start (point)))
+           (loop for i from 1 to notmuch-hello-recent-searches-max
+                 for search in notmuch-search-history do
+                   (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i))))
                      (set widget-symbol
                           (widget-create 'editable-field
                                          ;; Don't let the search boxes be
@@ -550,9 +545,7 @@ Complete list of currently available key bindings:
                                               (notmuch-hello-add-saved-search widget))
                                     :notmuch-saved-search-widget widget-symbol
                                     "save"))
-                   (widget-insert "\n")
-                   (setq nth (1+ nth)))
-                 notmuch-hello-recent-searches)
+                   (widget-insert "\n"))
            (indent-rigidly start (point) notmuch-hello-indent)))
 
        (when alltags-alist
@@ -565,29 +558,29 @@ Complete list of currently available key bindings:
          (widget-insert "\n\n")
          (let ((start (point)))
            (setq found-target-pos (notmuch-hello-insert-tags alltags-alist widest target))
-           (if (not final-target-pos)
-               (setq final-target-pos found-target-pos))
+           (unless final-target-pos
+             (setq final-target-pos found-target-pos))
            (indent-rigidly start (point) notmuch-hello-indent)))
 
        (widget-insert "\n")
 
-       (if (not notmuch-show-all-tags-list)
-           (widget-create 'push-button
-                          :notify (lambda (widget &rest ignore)
-                                    (setq notmuch-show-all-tags-list t)
-                                    (notmuch-hello-update))
-                          "Show all tags")))
+       (unless notmuch-show-all-tags-list
+         (widget-create 'push-button
+                        :notify (lambda (widget &rest ignore)
+                                  (setq notmuch-show-all-tags-list t)
+                                  (notmuch-hello-update))
+                        "Show all tags")))
 
       (let ((start (point)))
        (widget-insert "\n\n")
        (widget-insert "Type a search query and hit RET to view matching threads.\n")
-       (when notmuch-hello-recent-searches
+       (when notmuch-search-history
          (widget-insert "Hit RET to re-submit a previous search. Edit it first if you like.\n")
          (widget-insert "Save recent searches with the `save' button.\n"))
        (when notmuch-saved-searches
          (widget-insert "Edit saved searches with the `edit' button.\n"))
        (widget-insert "Hit RET or click on a saved search or tag name to view matching threads.\n")
-       (widget-insert "`=' refreshes this screen. `s' jumps to the search box. `q' to quit.\n")
+       (widget-insert "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
        (let ((fill-column (- (window-width) notmuch-hello-indent)))
          (center-region start (point))))
 
@@ -599,7 +592,7 @@ Complete list of currently available key bindings:
          (widget-forward 1)))
 
       (unless (widget-at)
-       (notmuch-hello-goto-search))))
+       (goto-char default-pos))))
 
   (run-hooks 'notmuch-hello-refresh-hook))
 
index 0f856bf0035e8bceb02cd2bb83a0be800c666dec..d315f7656e9bc4a004575faa14be386a858cdb83 100644 (file)
   "Notmuch mail reader for Emacs."
   :group 'mail)
 
+(defgroup notmuch-hello nil
+  "Overview of saved searches, tags, etc."
+  :group 'notmuch)
+
+(defgroup notmuch-search nil
+  "Searching and sorting mail."
+  :group 'notmuch)
+
+(defgroup notmuch-show nil
+  "Showing messages and threads."
+  :group 'notmuch)
+
+(defgroup notmuch-send nil
+  "Sending messages from Notmuch."
+  :group 'notmuch)
+
+(custom-add-to-group 'notmuch-send 'message 'custom-group)
+
+(defgroup notmuch-crypto nil
+  "Processing and display of cryptographic MIME parts."
+  :group 'notmuch)
+
+(defgroup notmuch-hooks nil
+  "Running custom code on well-defined occasions."
+  :group 'notmuch)
+
+(defgroup notmuch-external nil
+  "Running external commands from within Notmuch."
+  :group 'notmuch)
+
+(defgroup notmuch-faces nil
+  "Graphical attributes for displaying text"
+  :group 'notmuch)
+
 (defcustom notmuch-search-oldest-first t
   "Show the oldest mail first when searching."
   :type 'boolean
-  :group 'notmuch)
+  :group 'notmuch-search)
 
 ;;
 
+(defvar notmuch-search-history nil
+  "Variable to store notmuch searches history.")
+
 (defcustom notmuch-saved-searches nil
   "A list of saved searches to display."
   :type '(alist :key-type string :value-type string)
-  :group 'notmuch)
+  :group 'notmuch-hello)
 
 (defvar notmuch-folders nil
   "Deprecated name for what is now known as `notmuch-saved-searches'.")
@@ -96,6 +133,15 @@ the user hasn't set this variable with the old or new value."
   (interactive)
   (kill-buffer (current-buffer)))
 
+(defun notmuch-prettify-subject (subject)
+  ;; This function is used by `notmuch-search-process-filter' which
+  ;; requires that we not disrupt its' matching state.
+  (save-match-data
+    (if (and subject
+            (string-match "^[ \t]*$" subject))
+       "[No Subject]"
+      subject)))
+
 ;;
 
 (defun notmuch-common-do-stash (text)
@@ -114,14 +160,14 @@ the user hasn't set this variable with the old or new value."
       (setq list (cdr list)))
     (nreverse out)))
 
-; This lets us avoid compiling these replacement functions when emacs
-; is sufficiently new enough to supply them alone. We do the macro
-; treatment rather than just wrapping our defun calls in a when form
-; specifically so that the compiler never sees the code on new emacs,
-; (since the code is triggering warnings that we don't know how to get
-; rid of.
-;
-; A more clever macro here would accept a condition and a list of forms.
+;; This lets us avoid compiling these replacement functions when emacs
+;; is sufficiently new enough to supply them alone. We do the macro
+;; treatment rather than just wrapping our defun calls in a when form
+;; specifically so that the compiler never sees the code on new emacs,
+;; (since the code is triggering warnings that we don't know how to get
+;; rid of.
+;;
+;; A more clever macro here would accept a condition and a list of forms.
 (defmacro compile-on-emacs-prior-to-23 (form)
   "Conditionally evaluate form only on emacs < emacs-23."
   (list 'when (< emacs-major-version 23)
index 6fbf82d220b667286c63d13615f814931b1ee5be..dcfbc4b373fddbddb1d88588913f73f063c5cc22 100644 (file)
@@ -51,13 +51,13 @@ the database.path option in the notmuch configuration file).
 You will be prompted to create the directory if it does not exist
 yet when sending a mail."
 
- :require 'notmuch-fcc-initialization
- :group 'notmuch
  :type '(choice
         (const :tag "No FCC header" nil)
         (string :tag "A single folder")
         (repeat :tag "A folder based on the From header"
-                (cons regexp (string :tag "Folder")))))
+                (cons regexp (string :tag "Folder"))))
+ :require 'notmuch-fcc-initialization
+ :group 'notmuch-send)
 
 (defun notmuch-fcc-initialization ()
   "If notmuch-fcc-directories is set,
index 08e5b1749af8e4d11813826f23675f6c6676806a..264a5b9b496f483df94e0dde844b7d926712a6e2 100644 (file)
@@ -31,7 +31,7 @@ For example, if you wanted to add a \"replied\" tag and remove
 the \"inbox\" and \"todo\", you would set
     (\"replied\" \"-inbox\" \"-todo\"\)"
   :type 'list
-  :group 'notmuch)
+  :group 'notmuch-send)
 
 (defun notmuch-message-mark-replied ()
   ;; get the in-reply-to header and parse it for the message id.
index 3e93d7c8bfbf7945dcbc0fc36898a6ba2806e8b0..c07b67ba3d389d8b44fbc3b248959baa0107b810 100644 (file)
 
 (defcustom notmuch-mua-send-hook '(notmuch-mua-message-send-hook)
   "Hook run before sending messages."
-  :group 'notmuch
-  :type 'hook)
+  :type 'hook
+  :group 'notmuch-send
+  :group 'notmuch-hooks)
 
 (defcustom notmuch-mua-user-agent-function 'notmuch-mua-user-agent-full
   "Function used to generate a `User-Agent:' string. If this is
 `nil' then no `User-Agent:' will be generated."
-  :group 'notmuch
   :type '(choice (const :tag "No user agent string" nil)
                 (const :tag "Full" notmuch-mua-user-agent-full)
                 (const :tag "Notmuch" notmuch-mua-user-agent-notmuch)
                 (const :tag "Emacs" notmuch-mua-user-agent-emacs)
                 (function :tag "Custom user agent function"
-                          :value notmuch-mua-user-agent-full)))
+                          :value notmuch-mua-user-agent-full))
+  :group 'notmuch-send)
 
 (defcustom notmuch-mua-hidden-headers '("^User-Agent:")
   "Headers that are added to the `message-mode' hidden headers
 list."
-  :group 'notmuch
-  :type '(repeat string))
+  :type '(repeat string)
+  :group 'notmuch-send)
 
 ;;
 
@@ -71,12 +72,15 @@ list."
            (push header message-hidden-headers)))
        notmuch-mua-hidden-headers))
 
-(defun notmuch-mua-reply (query-string &optional sender)
+(defun notmuch-mua-reply (query-string &optional sender reply-all)
   (let (headers
        body
        (args '("reply")))
     (if notmuch-show-process-crypto
        (setq args (append args '("--decrypt"))))
+    (if reply-all
+       (setq args (append args '("--reply-to=all")))
+      (setq args (append args '("--reply-to=sender"))))
     (setq args (append args (list query-string)))
     ;; This make assumptions about the output of `notmuch reply', but
     ;; really only that the headers come first followed by a blank
@@ -108,7 +112,8 @@ list."
     (if (re-search-backward message-signature-separator nil t)
          (forward-line -1)
       (goto-char (point-max)))
-    (insert body))
+    (insert body)
+    (push-mark))
   (set-buffer-modified-p nil)
 
   (message-goto-body)
@@ -158,16 +163,16 @@ OTHER-ARGS are passed through to `message-mail'."
 
 If this variable is left unset, then a list will be constructed from the
 name and addresses configured in the notmuch configuration file."
-  :group 'notmuch
-  :type '(repeat string))
+  :type '(repeat string)
+  :group 'notmuch-send)
 
 (defcustom notmuch-always-prompt-for-sender nil
   "Always prompt for the From: address when composing or forwarding a message.
 
 This is not taken into account when replying to a message, because in that case
 the From: header is already filled in by notmuch."
-  :group 'notmuch
-  :type 'boolean)
+  :type 'boolean
+  :group 'notmuch-send)
 
 (defvar notmuch-mua-sender-history nil)
 
@@ -222,13 +227,13 @@ the From: address first."
        (notmuch-mua-forward-message))
     (notmuch-mua-forward-message)))
 
-(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender)
+(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all)
   "Invoke the notmuch reply window."
   (interactive "P")
   (let ((sender
         (when prompt-for-sender
           (notmuch-mua-prompt-for-sender))))
-    (notmuch-mua-reply query-string sender)))
+    (notmuch-mua-reply query-string sender reply-all)))
 
 (defun notmuch-mua-send-and-exit (&optional arg)
   (interactive "P")
diff --git a/emacs/notmuch-print.el b/emacs/notmuch-print.el
new file mode 100644 (file)
index 0000000..6653d97
--- /dev/null
@@ -0,0 +1,92 @@
+;; notmuch-print.el --- printing messages from notmuch.
+;;
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+
+(require 'notmuch-lib)
+
+(declare-function notmuch-show-get-prop "notmuch-show" (prop &optional props))
+
+(defcustom notmuch-print-mechanism 'notmuch-print-lpr
+  "How should printing be done?"
+  :group 'notmuch
+  :type '(choice
+         (function :tag "Use lpr" notmuch-print-lpr)
+         (function :tag "Use ps-print" notmuch-print-ps-print)
+         (function :tag "Use ps-print then evince" notmuch-print-ps-print/evince)
+         (function :tag "Use muttprint" notmuch-print-muttprint)
+         (function :tag "Use muttprint then evince" notmuch-print-muttprint/evince)
+         (function :tag "Using a custom function")))
+
+;; Utility functions:
+
+(defun notmuch-print-run-evince (file)
+  "View FILE using 'evince'."
+  (start-process "evince" nil "evince" file))
+
+(defun notmuch-print-run-muttprint (&optional output)
+  "Pass the contents of the current buffer to 'muttprint'.
+
+Optional OUTPUT allows passing a list of flags to muttprint."
+  (apply #'call-process-region (point-min) (point-max)
+        ;; Reads from stdin.
+        "muttprint"
+        nil nil nil
+        ;; Show the tags.
+        "--printed-headers" "Date_To_From_CC_Newsgroups_*Subject*_/Tags/"
+        output))
+
+;; User-visible functions:
+
+(defun notmuch-print-lpr (msg)
+  "Print a message buffer using lpr."
+  (lpr-buffer))
+
+(defun notmuch-print-ps-print (msg)
+  "Print a message buffer using the ps-print package."
+  (let ((subject (notmuch-prettify-subject
+                 (plist-get (notmuch-show-get-prop :headers msg) :Subject))))
+    (rename-buffer subject t)
+    (ps-print-buffer)))
+
+(defun notmuch-print-ps-print/evince (msg)
+  "Preview a message buffer using ps-print and evince."
+  (let ((ps-file (make-temp-file "notmuch"))
+       (subject (notmuch-prettify-subject
+                 (plist-get (notmuch-show-get-prop :headers msg) :Subject))))
+    (rename-buffer subject t)
+    (ps-print-buffer ps-file)
+    (notmuch-print-run-evince ps-file)))
+
+(defun notmuch-print-muttprint (msg)
+  "Print a message using muttprint."
+  (notmuch-print-run-muttprint))
+
+(defun notmuch-print-muttprint/evince (msg)
+  "Preview a message buffer using muttprint and evince."
+  (let ((ps-file (make-temp-file "notmuch")))
+    (notmuch-print-run-muttprint (list "--printer" (concat "TO_FILE:" ps-file)))
+    (notmuch-print-run-evince ps-file)))
+
+(defun notmuch-print-message (msg)
+  "Print a message using the user-selected mechanism."
+  (set-buffer-modified-p nil)
+  (funcall notmuch-print-mechanism msg))
+
+(provide 'notmuch-print)
index 82d11c925bab4b5ecc6b225579e430514b8fce44..7469e2eb8feb6ee8f9ffe0afc8a94a74541e7257 100644 (file)
 (require 'notmuch-wash)
 (require 'notmuch-mua)
 (require 'notmuch-crypto)
+(require 'notmuch-print)
 
 (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
 (declare-function notmuch-fontify-headers "notmuch" nil)
 (declare-function notmuch-select-tag-with-completion "notmuch" (prompt &rest search-terms))
+(declare-function notmuch-search-next-thread "notmuch" nil)
 (declare-function notmuch-search-show-thread "notmuch" nil)
 
 (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
@@ -47,8 +49,8 @@ For an open message, all of these headers will be made visible
 according to `notmuch-message-headers-visible' or can be toggled
 with `notmuch-show-toggle-headers'. For a closed message, only
 the first header in the list will be visible."
-  :group 'notmuch
-  :type '(repeat string))
+  :type '(repeat string)
+  :group 'notmuch-show)
 
 (defcustom notmuch-message-headers-visible t
   "Should the headers be visible by default?
@@ -58,38 +60,44 @@ If this value is non-nil, then all of the headers defined in
 of each message. Otherwise, these headers will be hidden and
 `notmuch-show-toggle-headers' can be used to make the visible for
 any given message."
-  :group 'notmuch
-  :type 'boolean)
+  :type 'boolean
+  :group 'notmuch-show)
 
 (defcustom notmuch-show-relative-dates t
   "Display relative dates in the message summary line."
-  :group 'notmuch
-  :type 'boolean)
+  :type 'boolean
+  :group 'notmuch-show)
 
 (defvar notmuch-show-markup-headers-hook '(notmuch-show-colour-headers)
   "A list of functions called to decorate the headers listed in
 `notmuch-message-headers'.")
 
-(defcustom notmuch-show-hook nil
+(defcustom notmuch-show-hook '(notmuch-show-turn-on-visual-line-mode)
   "Functions called after populating a `notmuch-show' buffer."
-  :group 'notmuch
-  :type 'hook)
-
-(defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-excerpt-citations)
+  :type 'hook
+  :options '(notmuch-show-turn-on-visual-line-mode)
+  :group 'notmuch-show
+  :group 'notmuch-hooks)
+
+(defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-wrap-long-lines
+                                                notmuch-wash-tidy-citations
+                                                notmuch-wash-elide-blank-lines
+                                                notmuch-wash-excerpt-citations)
   "Functions used to improve the display of text/plain parts."
-  :group 'notmuch
   :type 'hook
   :options '(notmuch-wash-convert-inline-patch-to-part
             notmuch-wash-wrap-long-lines
             notmuch-wash-tidy-citations
             notmuch-wash-elide-blank-lines
-            notmuch-wash-excerpt-citations))
+            notmuch-wash-excerpt-citations)
+  :group 'notmuch-show
+  :group 'notmuch-hooks)
 
 ;; Mostly useful for debugging.
 (defcustom notmuch-show-all-multipart/alternative-parts t
   "Should all parts of multipart/alternative parts be shown?"
-  :group 'notmuch
-  :type 'boolean)
+  :type 'boolean
+  :group 'notmuch-show)
 
 (defcustom notmuch-show-indent-messages-width 1
   "Width of message indentation in threads.
@@ -98,14 +106,24 @@ Messages are shown indented according to their depth in a thread.
 This variable determines the width of this indentation measured
 in number of blanks.  Defaults to `1', choose `0' to disable
 indentation."
-  :group 'notmuch
-  :type 'integer)
+  :type 'integer
+  :group 'notmuch-show)
 
 (defcustom notmuch-show-indent-multipart nil
   "Should the sub-parts of a multipart/* part be indented?"
   ;; dme: Not sure which is a good default.
-  :group 'notmuch
-  :type 'boolean)
+  :type 'boolean
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-part-button-default-action 'notmuch-show-save-part
+  "Default part header button action (on ENTER or mouse click)."
+  :group 'notmuch-show
+  :type '(choice (const :tag "Save part"
+                       notmuch-show-save-part)
+                (const :tag "View part"
+                       notmuch-show-view-part)
+                (const :tag "View interactively"
+                       notmuch-show-interactively-view-part)))
 
 (defmacro with-current-notmuch-show-message (&rest body)
   "Evaluate body with current buffer set to the text of current message"
@@ -117,18 +135,22 @@ indentation."
            ,@body)
         (kill-buffer buf)))))
 
+(defun notmuch-show-turn-on-visual-line-mode ()
+  "Enable Visual Line mode."
+  (visual-line-mode t))
+
 (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 override the mm-inline-media-tests to indicate which message
-   ; parts are already sufficiently handled by the original
-   ; presentation of the message in notmuch-show mode. These parts
-   ; will be inserted directly into the temporary buffer of
-   ; 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.
+   ;; We override the mm-inline-media-tests to indicate which message
+   ;; parts are already sufficiently handled by the original
+   ;; presentation of the message in notmuch-show mode. These parts
+   ;; will be inserted directly into the temporary buffer of
+   ;; 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)
@@ -183,6 +205,52 @@ indentation."
       mm-handle (> (notmuch-count-attachments mm-handle) 1))))
   (message "Done"))
 
+(defun notmuch-show-with-message-as-text (fn)
+  "Apply FN to a text representation of the current message.
+
+FN is called with one argument, the message properties. It should
+operation on the contents of the current buffer."
+
+  ;; Remake the header to ensure that all information is available.
+  (let* ((to (notmuch-show-get-to))
+        (cc (notmuch-show-get-cc))
+        (from (notmuch-show-get-from))
+        (subject (notmuch-show-get-subject))
+        (date (notmuch-show-get-date))
+        (tags (notmuch-show-get-tags))
+        (depth (notmuch-show-get-depth))
+
+        (header (concat
+                 "Subject: " subject "\n"
+                 "To: " to "\n"
+                 (if (not (string= cc ""))
+                     (concat "Cc: " cc "\n")
+                   "")
+                 "From: " from "\n"
+                 "Date: " date "\n"
+                 (if tags
+                     (concat "Tags: "
+                             (mapconcat #'identity tags ", ") "\n")
+                   "")))
+        (all (buffer-substring (notmuch-show-message-top)
+                               (notmuch-show-message-bottom)))
+
+        (props (notmuch-show-get-message-properties)))
+    (with-temp-buffer
+      (insert all)
+      (indent-rigidly (point-min) (point-max) (- depth))
+      ;; Remove the original header.
+      (goto-char (point-min))
+      (re-search-forward "^$" (point-max) nil)
+      (delete-region (point-min) (point))
+      (insert header)
+      (funcall fn props))))
+
+(defun notmuch-show-print-message ()
+  "Print the current message."
+  (interactive)
+  (notmuch-show-with-message-as-text 'notmuch-print-message))
+
 (defun notmuch-show-fontify-header ()
   (let ((face (cond
               ((looking-at "[Tt]o:")
@@ -227,21 +295,57 @@ indentation."
   "Try to clean a single email ADDRESS for display.  Return
 unchanged ADDRESS if parsing fails."
   (condition-case nil
-    (let* ((parsed (mail-header-parse-address address))
-          (address (car parsed))
-          (name (cdr parsed)))
-      ;; Remove double quotes. They might be required during transport,
-      ;; but we don't need to see them.
-      (when name
-        (setq name (replace-regexp-in-string "\"" "" name)))
+    (let (p-name p-address)
+      ;; It would be convenient to use `mail-header-parse-address',
+      ;; but that expects un-decoded mailbox parts, whereas our
+      ;; mailbox parts are already decoded (and hence may contain
+      ;; UTF-8). Given that notmuch should handle most of the awkward
+      ;; cases, some simple string deconstruction should be sufficient
+      ;; here.
+      (cond
+       ;; "User <user@dom.ain>" style.
+       ((string-match "\\(.*\\) <\\(.*\\)>" address)
+       (setq p-name (match-string 1 address)
+             p-address (match-string 2 address)))
+
+       ;; "<user@dom.ain>" style.
+       ((string-match "<\\(.*\\)>" address)
+       (setq p-address (match-string 1 address)))
+
+       ;; Everything else.
+       (t
+       (setq p-address address)))
+
+      (when p-name
+       ;; Remove elements of the mailbox part that are not relevant for
+       ;; display, even if they are required during transport:
+       ;;
+       ;; Backslashes.
+       (setq p-name (replace-regexp-in-string "\\\\" "" p-name))
+
+       ;; Outer single and double quotes, which might be nested.
+       (loop
+        with start-of-loop
+        do (setq start-of-loop p-name)
+
+        when (string-match "^\"\\(.*\\)\"$" p-name)
+        do (setq p-name (match-string 1 p-name))
+
+        when (string-match "^'\\(.*\\)'$" p-name)
+        do (setq p-name (match-string 1 p-name))
+
+        until (string= start-of-loop p-name)))
+
       ;; If the address is 'foo@bar.com <foo@bar.com>' then show just
       ;; 'foo@bar.com'.
-      (when (string= name address)
-        (setq name nil))
-
-      (if (not name)
-        address
-        (concat name " <" address ">")))
+      (when (string= p-name p-address)
+       (setq p-name nil))
+
+      ;; If no name results, return just the address.
+      (if (not p-name)
+         p-address
+       ;; Otherwise format the name and address together.
+       (concat p-name " <" p-address ">")))
     (error address)))
 
 (defun notmuch-show-insert-headerline (headers date tags depth)
@@ -278,10 +382,21 @@ message at DEPTH in the current thread."
        (run-hooks 'notmuch-show-markup-headers-hook)))))
 
 (define-button-type 'notmuch-show-part-button-type
-  'action 'notmuch-show-part-button-action
+  'action 'notmuch-show-part-button-default
+  'keymap 'notmuch-show-part-button-map
   'follow-link t
   'face 'message-mml)
 
+(defvar notmuch-show-part-button-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map button-map)
+    (define-key map "s" 'notmuch-show-part-button-save)
+    (define-key map "v" 'notmuch-show-part-button-view)
+    (define-key map "o" 'notmuch-show-part-button-interactively-view)
+    map)
+  "Submap for button commands")
+(fset 'notmuch-show-part-button-map notmuch-show-part-button-map)
+
 (defun notmuch-show-insert-part-header (nth content-type declared-type &optional name comment)
   (let ((button))
     (setq button
@@ -296,44 +411,75 @@ message at DEPTH in the current thread."
                   " ]")
           :type 'notmuch-show-part-button-type
           :notmuch-part nth
-          :notmuch-filename name))
+          :notmuch-filename name
+          :notmuch-content-type content-type))
     (insert "\n")
     ;; return button
     button))
 
 ;; Functions handling particular MIME parts.
 
-(defun notmuch-show-save-part (message-id nth &optional filename)
-  (let ((process-crypto notmuch-show-process-crypto))
-    (with-temp-buffer
-      (setq notmuch-show-process-crypto process-crypto)
-      ;; Always acquires the part via `notmuch part', even if it is
-      ;; available in the JSON output.
-      (insert (notmuch-show-get-bodypart-internal message-id nth))
-      (let ((file (read-file-name
-                  "Filename to save as: "
-                  (or mailcap-download-directory "~/")
-                  nil nil
-                  filename)))
-       ;; Don't re-compress .gz & al.  Arguably we should make
-       ;; `file-name-handler-alist' nil, but that would chop
-       ;; ange-ftp, which is reasonable to use here.
-       (mm-write-region (point-min) (point-max) file nil nil nil 'no-conversion t)))))
+(defmacro notmuch-with-temp-part-buffer (message-id nth &rest body)
+  (declare (indent 2))
+  (let ((process-crypto (make-symbol "process-crypto")))
+    `(let ((,process-crypto notmuch-show-process-crypto))
+       (with-temp-buffer
+        (setq notmuch-show-process-crypto ,process-crypto)
+        ;; Always acquires the part via `notmuch part', even if it is
+        ;; available in the JSON output.
+        (insert (notmuch-show-get-bodypart-internal ,message-id ,nth))
+        ,@body))))
+
+(defun notmuch-show-save-part (message-id nth &optional filename content-type)
+  (notmuch-with-temp-part-buffer message-id nth
+    (let ((file (read-file-name
+                "Filename to save as: "
+                (or mailcap-download-directory "~/")
+                nil nil
+                filename)))
+      ;; Don't re-compress .gz & al.  Arguably we should make
+      ;; `file-name-handler-alist' nil, but that would chop
+      ;; ange-ftp, which is reasonable to use here.
+      (mm-write-region (point-min) (point-max) file nil nil nil 'no-conversion t))))
+
+(defun notmuch-show-view-part (message-id nth &optional filename content-type )
+  (notmuch-with-temp-part-buffer message-id nth
+    ;; set mm-inlined-types to nil to force an external viewer
+    (let ((handle (mm-make-handle (current-buffer) (list content-type)))
+         (mm-inlined-types nil))
+      ;; We override mm-save-part as notmuch-show-save-part is better
+      ;; since it offers the filename. We need to lexically bind
+      ;; everything we need for notmuch-show-save-part to prevent
+      ;; potential dynamic shadowing.
+      (lexical-let ((message-id message-id)
+                   (nth nth)
+                   (filename filename)
+                   (content-type content-type))
+       (flet ((mm-save-part (&rest args) (notmuch-show-save-part
+                                          message-id nth filename content-type)))
+         (mm-display-part handle))))))
+
+(defun notmuch-show-interactively-view-part (message-id nth &optional filename content-type)
+  (notmuch-with-temp-part-buffer message-id nth
+    (let ((handle (mm-make-handle (current-buffer) (list content-type))))
+      (mm-interactively-view-part handle))))
 
 (defun notmuch-show-mm-display-part-inline (msg part nth content-type)
   "Use the mm-decode/mm-view functions to display a part in the
 current buffer, if possible."
   (let ((display-buffer (current-buffer)))
     (with-temp-buffer
-      (let ((handle (mm-make-handle (current-buffer) (list content-type))))
-       (if (and (mm-inlinable-p handle)
-                (mm-inlined-p handle))
-           (let ((content (notmuch-show-get-bodypart-content msg part nth)))
-             (insert content)
-             (set-buffer display-buffer)
-             (mm-display-part handle)
-             t)
-         nil)))))
+      (let* ((charset (plist-get part :content-charset))
+            (handle (mm-make-handle (current-buffer) `(,content-type (charset . ,charset)))))
+       ;; If the user wants the part inlined, insert the content and
+       ;; test whether we are able to inline it (which includes both
+       ;; capability and suitability tests).
+       (when (mm-inlined-p handle)
+         (insert (notmuch-show-get-bodypart-content msg part nth))
+         (when (mm-inlinable-p handle)
+           (set-buffer display-buffer)
+           (mm-display-part handle)
+           t))))))
 
 (defvar notmuch-show-multipart/alternative-discouraged
   '(
@@ -585,6 +731,10 @@ current buffer, if possible."
                nil))
          nil))))
 
+;; Handler for wash generated inline patch fake parts.
+(defun notmuch-show-insert-part-inline-patch-fake-part (msg part content-type nth depth declared-type)
+  (notmuch-show-insert-part-*/* msg part "text/x-diff" nth depth "inline patch"))
+
 (defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
   ;; This handler _must_ succeed - it is the handler of last resort.
   (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename))
@@ -652,8 +802,8 @@ current buffer, if possible."
   ;; part, so we make sure that we're down at the end.
   (goto-char (point-max))
   ;; Ensure that the part ends with a carriage return.
-  (if (not (bolp))
-      (insert "\n")))
+  (unless (bolp)
+    (insert "\n")))
 
 (defun notmuch-show-insert-body (msg body depth)
   "Insert the body BODY at depth DEPTH in the current thread."
@@ -715,8 +865,6 @@ current buffer, if possible."
     ;; compatible with the existing implementation. This just sets it
     ;; to after the first header.
     (notmuch-show-insert-headers headers)
-    ;; Headers should include a blank line (backwards compatibility).
-    (insert "\n")
     (save-excursion
       (goto-char content-start)
       ;; If the subject of this message is the same as that of the
@@ -731,10 +879,12 @@ current buffer, if possible."
     (setq notmuch-show-previous-subject bare-subject)
 
     (setq body-start (point-marker))
+    ;; A blank line between the headers and the body.
+    (insert "\n")
     (notmuch-show-insert-body msg (plist-get msg :body) depth)
     ;; Ensure that the body ends with a newline.
-    (if (not (bolp))
-       (insert "\n"))
+    (unless (bolp)
+      (insert "\n"))
     (setq body-end (point-marker))
     (setq content-end (point-marker))
 
@@ -753,6 +903,8 @@ current buffer, if possible."
       (overlay-put headers-overlay 'priority 10))
     (overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
 
+    (plist-put msg :depth depth)
+
     ;; Save the properties for this message. Currently this saves the
     ;; entire message (augmented it with other stuff), which seems
     ;; like overkill. We might save a reduced subset (for example, not
@@ -869,17 +1021,14 @@ buffer."
 
       (jit-lock-register #'notmuch-show-buttonise-links)
 
-      ;; Act on visual lines rather than logical lines.
-      (visual-line-mode t)
-
       (run-hooks 'notmuch-show-hook))
 
     ;; Move straight to the first open message
-    (if (not (notmuch-show-message-visible-p))
-       (notmuch-show-next-open-message))
+    (unless (notmuch-show-message-visible-p)
+      (notmuch-show-next-open-message))
 
     ;; Set the header line to the subject of the first open message.
-    (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))
+    (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-pretty-subject)))
 
     (notmuch-show-mark-read)))
 
@@ -925,7 +1074,8 @@ thread id.  If a prefix is given, crypto processing is toggled."
        (define-key map "s" 'notmuch-search)
        (define-key map "m" 'notmuch-mua-new-mail)
        (define-key map "f" 'notmuch-show-forward-message)
-       (define-key map "r" 'notmuch-show-reply)
+       (define-key map "r" 'notmuch-show-reply-sender)
+       (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)
@@ -936,7 +1086,8 @@ thread id.  If a prefix is given, crypto processing is toggled."
        (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 "a" 'notmuch-show-archive-message-then-next)
+       (define-key map "A" 'notmuch-show-archive-thread-then-next)
        (define-key map "N" 'notmuch-show-next-message)
        (define-key map "P" 'notmuch-show-previous-message)
        (define-key map "n" 'notmuch-show-next-open-message)
@@ -945,6 +1096,7 @@ thread id.  If a prefix is given, crypto processing is toggled."
        (define-key map " " 'notmuch-show-advance-and-archive)
        (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
        (define-key map (kbd "RET") 'notmuch-show-toggle-message)
+       (define-key map "#" 'notmuch-show-print-message)
        map)
       "Keymap for \"notmuch show\" buffers.")
 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
@@ -982,7 +1134,8 @@ All currently available key bindings:
   (use-local-map notmuch-show-mode-map)
   (setq major-mode 'notmuch-show-mode
        mode-name "notmuch-show")
-  (setq buffer-read-only t))
+  (setq buffer-read-only t
+       truncate-lines t))
 
 (defun notmuch-show-move-to-message-top ()
   (goto-char (notmuch-show-message-top)))
@@ -1104,6 +1257,12 @@ Some useful entries are:
 (defun notmuch-show-get-to ()
   (notmuch-show-get-header :To))
 
+(defun notmuch-show-get-depth ()
+  (notmuch-show-get-prop :depth))
+
+(defun notmuch-show-get-pretty-subject ()
+  (notmuch-prettify-subject (notmuch-show-get-subject)))
+
 (defun notmuch-show-set-tags (tags)
   "Set the tags of the current message."
   (notmuch-show-set-prop :tags tags)
@@ -1191,7 +1350,7 @@ thread from the search from which this thread was originally
 shown."
   (interactive)
   (if (notmuch-show-advance)
-      (notmuch-show-archive-thread)))
+      (notmuch-show-archive-thread-then-next)))
 
 (defun notmuch-show-rewind ()
   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
@@ -1218,11 +1377,10 @@ any effects from previous calls to
       ;; If a small number of lines from the previous message are
       ;; visible, realign so that the top of the current message is at
       ;; the top of the screen.
-      (if (<= (count-screen-lines (window-start) start-of-message)
-             next-screen-context-lines)
-         (progn
-           (goto-char (notmuch-show-message-top))
-           (notmuch-show-message-adjust)))
+      (when (<= (count-screen-lines (window-start) start-of-message)
+               next-screen-context-lines)
+       (goto-char (notmuch-show-message-top))
+       (notmuch-show-message-adjust))
       ;; Move to the top left of the window.
       (goto-char (window-start)))
      (t
@@ -1230,9 +1388,14 @@ any effects from previous calls to
       (notmuch-show-previous-message)))))
 
 (defun notmuch-show-reply (&optional prompt-for-sender)
-  "Reply to the current message."
+  "Reply to the sender and all recipients of the current message."
+  (interactive "P")
+  (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender t))
+
+(defun notmuch-show-reply-sender (&optional prompt-for-sender)
+  "Reply to the sender of the current message."
   (interactive "P")
-  (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender))
+  (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender nil))
 
 (defun notmuch-show-forward-message (&optional prompt-for-sender)
   "Forward the current message."
@@ -1240,14 +1403,19 @@ any effects from previous calls to
   (with-current-notmuch-show-message
    (notmuch-mua-new-forward-message prompt-for-sender)))
 
-(defun notmuch-show-next-message ()
-  "Show the next message."
-  (interactive)
+(defun notmuch-show-next-message (&optional pop-at-end)
+  "Show the next message.
+
+If a prefix argument is given and this is the last message in the
+thread, navigate to the next thread in the parent search buffer."
+  (interactive "P")
   (if (notmuch-show-goto-message-next)
       (progn
        (notmuch-show-mark-read)
        (notmuch-show-message-adjust))
-    (goto-char (point-max))))
+    (if pop-at-end
+       (notmuch-show-next-thread)
+      (goto-char (point-max)))))
 
 (defun notmuch-show-previous-message ()
   "Show the previous message."
@@ -1256,9 +1424,13 @@ any effects from previous calls to
   (notmuch-show-mark-read)
   (notmuch-show-message-adjust))
 
-(defun notmuch-show-next-open-message ()
-  "Show the next message."
-  (interactive)
+(defun notmuch-show-next-open-message (&optional pop-at-end)
+  "Show the next open message.
+
+If a prefix argument is given and this is the last open message
+in the thread, navigate to the next thread in the parent search
+buffer."
+  (interactive "P")
   (let (r)
     (while (and (setq r (notmuch-show-goto-message-next))
                (not (notmuch-show-message-visible-p))))
@@ -1266,10 +1438,12 @@ any effects from previous calls to
        (progn
          (notmuch-show-mark-read)
          (notmuch-show-message-adjust))
-      (goto-char (point-max)))))
+      (if pop-at-end
+         (notmuch-show-next-thread)
+       (goto-char (point-max))))))
 
 (defun notmuch-show-previous-open-message ()
-  "Show the previous message."
+  "Show the previous open message."
   (interactive)
   (while (and (notmuch-show-goto-message-previous)
              (not (notmuch-show-message-visible-p))))
@@ -1300,7 +1474,7 @@ than only the current message."
   (interactive "P\nsPipe message to command: ")
   (let (shell-command)
     (if entire-thread
-       (setq shell-command 
+       (setq shell-command
              (concat notmuch-command " show --format=mbox "
                      (shell-quote-argument
                       (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
@@ -1407,23 +1581,45 @@ argument, hide all of the messages."
   (interactive)
   (backward-button 1))
 
-(defun notmuch-show-archive-thread-internal (show-next)
-  ;; Remove the tag from the current set of messages.
+(defun notmuch-show-tag-thread-internal (tag &optional remove)
+  "Add tag to the current set of messages.
+
+If the remove switch is given, tags will be removed instead of
+added."
   (goto-char (point-min))
-  (loop do (notmuch-show-remove-tag "inbox")
-       until (not (notmuch-show-goto-message-next)))
-  ;; Move to the next item in the search results, if any.
+  (let ((tag-function (if remove
+                         'notmuch-show-remove-tag
+                       'notmuch-show-add-tag)))
+    (loop do (funcall tag-function tag)
+         until (not (notmuch-show-goto-message-next)))))
+
+(defun notmuch-show-add-tag-thread (tag)
+  "Add tag to all messages in the current thread."
+  (interactive)
+  (notmuch-show-tag-thread-internal tag))
+
+(defun notmuch-show-remove-tag-thread (tag)
+  "Remove tag from all messages in the current thread."
+  (interactive)
+  (notmuch-show-tag-thread-internal tag t))
+
+(defun notmuch-show-next-thread (&optional show-next)
+  "Move to the next item in the search results, if any."
+  (interactive "P")
   (let ((parent-buffer notmuch-show-parent-buffer))
     (notmuch-kill-this-buffer)
-    (if parent-buffer
-       (progn
-         (switch-to-buffer parent-buffer)
-         (forward-line)
-         (if show-next
-             (notmuch-search-show-thread))))))
+    (when parent-buffer
+      (switch-to-buffer parent-buffer)
+      (notmuch-search-next-thread)
+      (if show-next
+         (notmuch-search-show-thread)))))
+
+(defun notmuch-show-archive-thread (&optional unarchive)
+  "Archive each message in thread.
 
-(defun notmuch-show-archive-thread ()
-  "Archive each message in thread, then show next thread from search.
+If a prefix argument is given, the messages will be
+\"unarchived\" (ie. the \"inbox\" tag will be added instead of
+removed).
 
 Archive each message currently shown by removing the \"inbox\"
 tag from each. Then kill this buffer and show the next thread
@@ -1433,13 +1629,39 @@ Note: This command is safe from any race condition of new messages
 being delivered to the same thread. It does not archive the
 entire thread, but only the messages shown in the current
 buffer."
+  (interactive "P")
+  (if unarchive
+      (notmuch-show-add-tag-thread "inbox")
+    (notmuch-show-remove-tag-thread "inbox")))
+
+(defun notmuch-show-archive-thread-then-next ()
+  "Archive each message in thread, then show next thread from search."
   (interactive)
-  (notmuch-show-archive-thread-internal t))
+  (notmuch-show-archive-thread)
+  (notmuch-show-next-thread t))
 
 (defun notmuch-show-archive-thread-then-exit ()
   "Archive each message in thread, then exit back to search results."
   (interactive)
-  (notmuch-show-archive-thread-internal nil))
+  (notmuch-show-archive-thread)
+  (notmuch-show-next-thread))
+
+(defun notmuch-show-archive-message (&optional unarchive)
+  "Archive the current message.
+
+If a prefix argument is given, the message will be
+\"unarchived\" (ie. the \"inbox\" tag will be added instead of
+removed)."
+  (interactive "P")
+  (if unarchive
+      (notmuch-show-add-tag "inbox")
+    (notmuch-show-remove-tag "inbox")))
+
+(defun notmuch-show-archive-message-then-next ()
+  "Archive the current message, then show the next open message in the current thread."
+  (interactive)
+  (notmuch-show-archive-message)
+  (notmuch-show-next-open-message t))
 
 (defun notmuch-show-stash-cc ()
   "Copy CC field of current message to kill-ring."
@@ -1488,12 +1710,30 @@ buffer."
 
 ;; Commands typically bound to buttons.
 
-(defun notmuch-show-part-button-action (button)
-  (let ((nth (button-get button :notmuch-part)))
-    (if nth
-       (notmuch-show-save-part (notmuch-show-get-message-id) nth
-                               (button-get button :notmuch-filename))
-      (message "Not a valid part (is it a fake part?)."))))
+(defun notmuch-show-part-button-default (&optional button)
+  (interactive)
+  (notmuch-show-part-button-internal button notmuch-show-part-button-default-action))
+
+(defun notmuch-show-part-button-save (&optional button)
+  (interactive)
+  (notmuch-show-part-button-internal button #'notmuch-show-save-part))
+
+(defun notmuch-show-part-button-view (&optional button)
+  (interactive)
+  (notmuch-show-part-button-internal button #'notmuch-show-view-part))
+
+(defun notmuch-show-part-button-interactively-view (&optional button)
+  (interactive)
+  (notmuch-show-part-button-internal button #'notmuch-show-interactively-view-part))
+
+(defun notmuch-show-part-button-internal (button handler)
+  (let ((button (or button (button-at (point)))))
+    (if button
+       (let ((nth (button-get button :notmuch-part)))
+         (if nth
+             (funcall handler (notmuch-show-get-message-id) nth
+                      (button-get button :notmuch-filename)
+                      (button-get button :notmuch-content-type)))))))
 
 ;;
 
index 1f420b25a05d770556cd19e4e737f522b7e18098..56981d0635aad98913ae90e46f88f3e739fa261d 100644 (file)
@@ -136,12 +136,13 @@ collapse the remaining lines into a button.")
         (lines-count (count-lines (overlay-start overlay) (overlay-end overlay))))
     (format label-format lines-count)))
 
-(defun notmuch-wash-region-to-button (msg beg end type prefix)
+(defun notmuch-wash-region-to-button (msg beg end type &optional prefix)
   "Auxiliary function to do the actual making of overlays and buttons
 
 BEG and END are buffer locations. TYPE should a string, either
-\"citation\" or \"signature\". PREFIX is some arbitrary text to
-insert before the button, probably for indentation."
+\"citation\" or \"signature\". Optional PREFIX is some arbitrary
+text to insert before the button, probably for indentation.  Note
+that PREFIX should not include a newline."
 
   ;; This uses some slightly tricky conversions between strings and
   ;; symbols because of the way the button code works. Note that
@@ -160,12 +161,15 @@ insert before the button, probably for indentation."
     (overlay-put overlay 'type type)
     (goto-char (1+ end))
     (save-excursion
-      (goto-char (1- beg))
-      (insert prefix)
-      (insert-button (notmuch-wash-button-label overlay)
+      (goto-char beg)
+      (if prefix
+         (insert-before-markers prefix))
+      (let ((button-beg (point)))
+       (insert-before-markers (notmuch-wash-button-label overlay) "\n")
+       (make-button button-beg (1- (point))
                     'invisibility-spec invis-spec
                     'overlay overlay
-                    :type button-type))))
+                    :type button-type)))))
 
 (defun notmuch-wash-excerpt-citations (msg depth)
   "Excerpt citations and up to one signature."
@@ -177,7 +181,7 @@ insert before the button, probably for indentation."
             (msg-end (point-max))
             (msg-lines (count-lines msg-start msg-end)))
        (notmuch-wash-region-to-button
-        msg msg-start msg-end "original" "\n")))
+        msg msg-start msg-end "original")))
   (while (and (< (point) (point-max))
              (re-search-forward notmuch-wash-citation-regexp nil t))
     (let* ((cite-start (match-beginning 0))
@@ -194,7 +198,7 @@ insert before the button, probably for indentation."
          (forward-line (- notmuch-wash-citation-lines-suffix))
          (notmuch-wash-region-to-button
           msg hidden-start (point-marker)
-          "citation" "\n")))))
+          "citation")))))
   (if (and (not (eobp))
           (re-search-forward notmuch-wash-signature-regexp nil t))
       (let* ((sig-start (match-beginning 0))
@@ -208,7 +212,7 @@ insert before the button, probably for indentation."
              (overlay-put (make-overlay sig-start-marker sig-end-marker) 'face 'message-cited-text)
              (notmuch-wash-region-to-button
               msg sig-start-marker sig-end-marker
-              "signature" "\n"))))))
+              "signature"))))))
 
 ;;
 
@@ -290,6 +294,44 @@ When doing so, maintaining citation leaders in the wrapped text."
 
 (defvar diff-file-header-re) ; From `diff-mode.el'.
 
+(defun notmuch-wash-subject-to-filename (subject &optional maxlen)
+  "Convert a mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\", without the leading patch sequence number
+\"0001-\" and \".patch\" extension. Any leading \"[PREFIX]\"
+style strings are removed prior to conversion.
+
+Optional argument MAXLEN is the maximum length of the resulting
+filename, before trimming any trailing . and - characters."
+  (let* ((s (replace-regexp-in-string "^ *\\(\\[[^]]*\\] *\\)*" "" subject))
+        (s (replace-regexp-in-string "[^A-Za-z0-9._]+" "-" s))
+        (s (replace-regexp-in-string "\\.+" "." s))
+        (s (if maxlen (substring s 0 (min (length s) maxlen)) s))
+        (s (replace-regexp-in-string "[.-]*$" "" s)))
+    s))
+
+(defun notmuch-wash-subject-to-patch-sequence-number (subject)
+  "Convert a patch mail SUBJECT into a patch sequence number.
+
+Return the patch sequence number N from the last \"[PATCH N/M]\"
+style prefix in SUBJECT, or nil if such a prefix can't be found."
+  (when (string-match
+        "^ *\\(\\[[^]]*\\] *\\)*\\[[^]]*?\\([0-9]+\\)/[0-9]+[^]]*\\].*"
+        subject)
+      (string-to-number (substring subject (match-beginning 2) (match-end 2)))))
+
+(defun notmuch-wash-subject-to-patch-filename (subject)
+  "Convert a patch mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\". If the patch mail was generated and sent using
+\"git format-patch/send-email\", this should re-create the
+original filename the sender had."
+  (format "%04d-%s.patch"
+         (or (notmuch-wash-subject-to-patch-sequence-number subject) 1)
+         (notmuch-wash-subject-to-filename subject 52)))
+
 (defun notmuch-wash-convert-inline-patch-to-part (msg depth)
   "Convert an inline patch into a fake 'text/x-diff' attachment.
 
@@ -298,27 +340,29 @@ patch and then guesses the extent of the patch, there is scope
 for error."
 
   (goto-char (point-min))
-  (if (re-search-forward diff-file-header-re nil t)
-      (progn
-       (beginning-of-line -1)
-       (let ((patch-start (point))
-             (patch-end (point-max))
-             part)
-         (goto-char patch-start)
-         (if (or
-              ;; Patch ends with signature.
-              (re-search-forward notmuch-wash-signature-regexp nil t)
-              ;; Patch ends with bugtraq comment.
-              (re-search-forward "^\\*\\*\\* " nil t))
-             (setq patch-end (match-beginning 0)))
-         (save-restriction
-           (narrow-to-region patch-start patch-end)
-           (setq part (plist-put part :content-type "text/x-diff"))
-           (setq part (plist-put part :content (buffer-string)))
-           (setq part (plist-put part :id -1))
-           (setq part (plist-put part :filename "inline patch"))
-           (delete-region (point-min) (point-max))
-           (notmuch-show-insert-bodypart nil part depth))))))
+  (when (re-search-forward diff-file-header-re nil t)
+    (beginning-of-line -1)
+    (let ((patch-start (point))
+         (patch-end (point-max))
+         part)
+      (goto-char patch-start)
+      (if (or
+          ;; Patch ends with signature.
+          (re-search-forward notmuch-wash-signature-regexp nil t)
+          ;; Patch ends with bugtraq comment.
+          (re-search-forward "^\\*\\*\\* " nil t))
+         (setq patch-end (match-beginning 0)))
+      (save-restriction
+       (narrow-to-region patch-start patch-end)
+       (setq part (plist-put part :content-type "inline-patch-fake-part"))
+       (setq part (plist-put part :content (buffer-string)))
+       (setq part (plist-put part :id -1))
+       (setq part (plist-put part :filename
+                             (notmuch-wash-subject-to-patch-filename
+                              (plist-get
+                               (plist-get msg :headers) :Subject))))
+       (delete-region (point-min) (point-max))
+       (notmuch-show-insert-bodypart nil part depth)))))
 
 ;;
 
index fde23779d7cf8f4a8853fd16da6940779cd48820..cd04ffdafcc4d46d36fa43248016c4270e157a5c 100644 (file)
@@ -1,53 +1,54 @@
-; 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).
+;; 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).
 
 (eval-when-compile (require 'cl))
+(require 'crm)
 (require 'mm-view)
 (require 'message)
 
@@ -70,17 +71,43 @@ For example:
        (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\)
                                             \(\"subject\" . \"%s\"\)\)\)"
   :type '(alist :key-type (string) :value-type (string))
-  :group 'notmuch)
+  :group 'notmuch-search)
 
 (defvar notmuch-query-history nil
   "Variable to store minibuffer history for notmuch queries")
 
-(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+(defun notmuch-tag-completions (&optional prefixes 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)))
+        (split-string
+         (with-output-to-string
+           (with-current-buffer standard-output
+             (apply 'call-process notmuch-command nil t
+                    nil "search-tags" search-terms)))
+         "\n+" t)))
+    (if (null prefixes)
+       tag-list
+      (apply #'append
+            (mapcar (lambda (tag)
+                      (mapcar (lambda (prefix)
+                                (concat prefix tag)) prefixes))
+                    tag-list)))))
+
+(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+  (let ((tag-list (notmuch-tag-completions nil search-terms)))
+    (completing-read prompt tag-list)))
+
+(defun notmuch-select-tags-with-completion (prompt &optional prefixes &rest search-terms)
+  (let ((tag-list (notmuch-tag-completions prefixes search-terms))
+       (crm-separator " ")
+       ;; By default, space is bound to "complete word" function.
+       ;; Re-bind it to insert a space instead.  Note that <tab>
+       ;; still does the completion.
+       (crm-local-completion-map
+        (let ((map (make-sparse-keymap)))
+          (set-keymap-parent map crm-local-completion-map)
+          (define-key map " " 'self-insert-command)
+          map)))
+    (delete "" (completing-read-multiple prompt tag-list))))
 
 (defun notmuch-foreach-mime-part (function mm-handle)
   (cond ((stringp (car mm-handle))
@@ -139,10 +166,10 @@ This is basically just `format-kbd-macro' but we also convert ESC to M-."
        "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.
+;; 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
@@ -164,16 +191,23 @@ For a mouse binding, return nil."
                "\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-one (key)
+  ;; A `keymap' key indicates inheritance from a parent keymap - the
+  ;; inherited mappings follow, so there is nothing to print for
+  ;; `keymap' itself.
+  (when (not (eq key 'keymap))
+    (notmuch-substitute-one-command-key-with-prefix nil key)))
 
 (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)))
+      (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
+            (keymap (symbol-value (intern keymap-name))))
+       (setq doc (replace-match
+                  (mapconcat #'notmuch-substitute-command-keys-one
+                             (cdr keymap) "\n")
+                  1 1 doc)))
       (setq beg (match-end 0)))
     doc))
 
@@ -192,7 +226,8 @@ For a mouse binding, return nil."
   "List of functions to call when notmuch displays the search results."
   :type 'hook
   :options '(hl-line-mode)
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-hooks)
 
 (defvar notmuch-search-mode-map
   (let ((map (make-sparse-keymap)))
@@ -206,7 +241,8 @@ For a mouse binding, return nil."
     (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 "r" 'notmuch-search-reply-to-thread-sender)
+    (define-key map "R" 'notmuch-search-reply-to-thread)
     (define-key map "m" 'notmuch-mua-new-mail)
     (define-key map "s" 'notmuch-search)
     (define-key map "o" 'notmuch-search-toggle-order)
@@ -262,14 +298,14 @@ For a mouse binding, return nil."
 (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.
+  ;; 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)))
@@ -299,27 +335,32 @@ For a mouse binding, return nil."
  '((((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)
+ :group 'notmuch-show
+ :group 'notmuch-faces)
 
 (defface notmuch-search-date
   '((t :inherit default))
   "Face used in search mode for dates."
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-faces)
 
 (defface notmuch-search-count
   '((t :inherit default))
   "Face used in search mode for the count matching the query."
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-faces)
 
 (defface notmuch-search-subject
   '((t :inherit default))
   "Face used in search mode for subjects."
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-faces)
 
 (defface notmuch-search-matching-authors
   '((t :inherit default))
   "Face used in search mode for authors matching the query."
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-faces)
 
 (defface notmuch-search-non-matching-authors
   '((((class color)
@@ -331,7 +372,8 @@ For a mouse binding, return nil."
     (t
      (:italic t)))
   "Face used in search mode for authors not matching the query."
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-faces)
 
 (defface notmuch-tag-face
   '((((class color)
@@ -343,7 +385,8 @@ For a mouse binding, return nil."
     (t
      (:bold t)))
   "Face used in search mode face for tags."
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-faces)
 
 (defun notmuch-search-mode ()
   "Major mode displaying results of a notmuch search.
@@ -424,27 +467,27 @@ Complete list of currently available key bindings:
   "Display the currently selected thread."
   (interactive "P")
   (let ((thread-id (notmuch-search-find-thread-id))
-       (subject (notmuch-search-find-subject)))
+       (subject (notmuch-prettify-subject (notmuch-search-find-subject))))
     (if (> (length thread-id) 0)
        (notmuch-show thread-id
                      (current-buffer)
                      notmuch-search-query-string
-                     ;; name the buffer based on notmuch-search-find-subject
-                     (if (string-match "^[ \t]*$" subject)
-                         "[No Subject]"
-                       (truncate-string-to-width
-                        (concat "*"
-                                (truncate-string-to-width subject 32 nil nil t)
-                                "*")
-                        32 nil nil t))
+                     ;; Name the buffer based on the subject.
+                     (concat "*" (truncate-string-to-width subject 30 nil nil t) "*")
                      crypto-switch)
-      (error "End of search results"))))
+      (message "End of search results."))))
 
 (defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
+  "Begin composing a reply-all to the entire current thread in a new buffer."
+  (interactive "P")
+  (let ((message-id (notmuch-search-find-thread-id)))
+    (notmuch-mua-new-reply message-id prompt-for-sender t)))
+
+(defun notmuch-search-reply-to-thread-sender (&optional prompt-for-sender)
   "Begin composing a reply to the entire current thread in a new buffer."
   (interactive "P")
   (let ((message-id (notmuch-search-find-thread-id)))
-    (notmuch-mua-new-reply message-id prompt-for-sender)))
+    (notmuch-mua-new-reply message-id prompt-for-sender nil)))
 
 (defun notmuch-call-notmuch-process (&rest args)
   "Synchronously invoke \"notmuch\" with the given list of arguments.
@@ -488,7 +531,7 @@ the messages that are about to be tagged"
 
   :type 'hook
   :options '(hl-line-mode)
-  :group 'notmuch)
+  :group 'notmuch-hooks)
 
 (defcustom notmuch-after-tag-hook nil
   "Hooks that are run after tags of a message are modified.
@@ -499,7 +542,7 @@ a list of strings of the form \"+TAG\" or \"-TAG\".
 the messages that were tagged"
   :type 'hook
   :options '(hl-line-mode)
-  :group 'notmuch)
+  :group 'notmuch-hooks)
 
 (defun notmuch-search-set-tags (tags)
   (save-excursion
@@ -603,7 +646,7 @@ thread or threads in the current region."
 This function advances the next thread when finished."
   (interactive)
   (notmuch-search-remove-tag-thread "inbox")
-  (forward-line))
+  (notmuch-search-next-thread))
 
 (defvar notmuch-search-process-filter-data nil
   "Data that has not yet been processed.")
@@ -624,17 +667,16 @@ This function advances the next thread when finished."
                  (goto-char (point-max))
                  (if (eq status 'signal)
                      (insert "Incomplete search results (search process was killed).\n"))
-                 (if (eq status 'exit)
-                     (progn
-                       (if notmuch-search-process-filter-data
-                           (insert (concat "Error: Unexpected output from notmuch search:\n" notmuch-search-process-filter-data)))
-                       (insert "End of search results.")
-                       (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))))))
+                 (when (eq status 'exit)
+                   (if notmuch-search-process-filter-data
+                       (insert (concat "Error: Unexpected output from notmuch search:\n" notmuch-search-process-filter-data)))
+                   (insert "End of search results.")
+                   (unless (= exit-status 0)
+                     (insert (format " (process returned %d)" exit-status)))
+                   (insert "\n")
+                   (if (and atbob
+                            (not (string= notmuch-search-target-thread "found")))
+                       (set 'never-found-target-thread t)))))
              (when (and never-found-target-thread
                       notmuch-search-target-line)
                  (goto-char (point-min))
@@ -655,7 +697,8 @@ attributes overriding earlier. A message having both \"delete\"
 and \"unread\" tags with the above settings would have a green
 foreground and blue background."
   :type '(alist :key-type (string) :value-type (custom-face-edit))
-  :group 'notmuch)
+  :group 'notmuch-search
+  :group 'notmuch-faces)
 
 (defun notmuch-search-color-line (start end line-tag-list)
   "Colorize lines in `notmuch-show' based on tags."
@@ -806,15 +849,15 @@ non-authors is found, assume that all of the authors match."
                      (if (/= (match-beginning 1) line)
                          (insert (concat "Error: Unexpected output from notmuch search:\n" (substring string line (match-beginning 1)) "\n")))
                      (let ((beg (point)))
-                       (notmuch-search-show-result date count authors subject tags)
+                       (notmuch-search-show-result date count authors
+                                                   (notmuch-prettify-subject subject) tags)
                        (notmuch-search-color-line beg (point) tag-list)
                        (put-text-property beg (point) 'notmuch-search-thread-id thread-id)
                        (put-text-property beg (point) 'notmuch-search-authors authors)
                        (put-text-property beg (point) 'notmuch-search-subject subject)
-                       (if (string= thread-id notmuch-search-target-thread)
-                           (progn
-                             (set 'found-target beg)
-                             (set 'notmuch-search-target-thread "found"))))
+                       (when (string= thread-id notmuch-search-target-thread)
+                         (set 'found-target beg)
+                         (set 'notmuch-search-target-thread "found")))
                      (set 'line (match-end 0)))
                  (set 'more nil)
                  (while (and (< line (length string)) (= (elt string line) ?\n))
@@ -826,7 +869,7 @@ non-authors is found, assume that all of the authors match."
              (goto-char found-target)))
       (delete-process proc))))
 
-(defun notmuch-search-operate-all (action)
+(defun notmuch-search-operate-all (&rest actions)
   "Add/remove tags from all matching messages.
 
 This command adds or removes tags from all messages matching the
@@ -837,16 +880,16 @@ will prompt for tags to be added or removed. Tags prefixed with
 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-tag notmuch-search-query-string action-split)))
+  (interactive (notmuch-select-tags-with-completion
+               "Operations (+add -drop): notmuch tag "
+               '("+" "-")))
+  ;; Perform some validation
+  (when (null actions) (error "No operations given"))
+  (mapc (lambda (action)
+         (unless (string-match-p "^[-+][-+_.[:word:]]+$" action)
+           (error "Action must be of the form `+this_tag' or `-that_tag'")))
+       actions)
+  (apply 'notmuch-tag notmuch-search-query-string actions))
 
 (defun notmuch-search-buffer-title (query)
   "Returns the title for a buffer with notmuch search results."
@@ -902,21 +945,25 @@ PROMPT is the string to prompt with."
               (t (list string)))))))
       ;; this was simpler than convincing completing-read to accept spaces:
       (define-key keymap (kbd "<tab>") 'minibuffer-complete)
-      (read-from-minibuffer prompt nil keymap nil
-                           'notmuch-query-history nil nil))))
+      (let ((history-delete-duplicates t))
+       (read-from-minibuffer prompt nil keymap nil
+                             'notmuch-search-history nil nil)))))
 
 ;;;###autoload
-(defun notmuch-search (query &optional oldest-first target-thread target-line continuation)
-  "Run \"notmuch search\" with the given query string and display results.
+(defun notmuch-search (&optional query oldest-first target-thread target-line continuation)
+  "Run \"notmuch search\" with the given `query' and display results.
 
-The optional parameters are used as follows:
+If `query' is nil, it is read interactively from the minibuffer.
+Other optional parameters are used as follows:
 
   oldest-first: A Boolean controlling the sort order of returned threads
   target-thread: A thread ID (with the thread: prefix) that will be made
                  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 (list (notmuch-read-query "Notmuch search: ")))
+  (interactive)
+  (if (null query)
+      (setq query (notmuch-read-query "Notmuch search: ")))
   (let ((buffer (get-buffer-create (notmuch-search-buffer-title query))))
     (switch-to-buffer buffer)
     (notmuch-search-mode)
@@ -990,7 +1037,7 @@ Note that the recommended way of achieving the same is using
   :type '(choice (const :tag "notmuch new" nil)
                 (const :tag "Disabled" "")
                 (string :tag "Custom script"))
-  :group 'notmuch)
+  :group 'notmuch-external)
 
 (defun notmuch-poll ()
   "Run \"notmuch new\" or an external script to import mail.
@@ -999,8 +1046,8 @@ Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
 depending on the value of `notmuch-poll-script'."
   (interactive)
   (if (stringp notmuch-poll-script)
-      (if (not (string= notmuch-poll-script ""))
-         (call-process notmuch-poll-script nil nil))
+      (unless (string= notmuch-poll-script "")
+       (call-process notmuch-poll-script nil nil))
     (call-process notmuch-command nil nil nil "new")))
 
 (defun notmuch-search-poll-and-refresh-view ()
@@ -1055,21 +1102,39 @@ current search results AND that are tagged with the given tag."
   (interactive)
   (notmuch-hello))
 
+(defun notmuch-interesting-buffer (b)
+  "Is the current buffer of interest to a notmuch user?"
+  (with-current-buffer b
+    (memq major-mode '(notmuch-show-mode
+                      notmuch-search-mode
+                      notmuch-hello-mode
+                      message-mode))))
+
 ;;;###autoload
-(defun notmuch-jump-to-recent-buffer ()
-  "Jump to the most recent notmuch buffer (search, show or hello).
+(defun notmuch-cycle-notmuch-buffers ()
+  "Cycle through any existing notmuch buffers (search, show or hello).
 
-If no recent buffer is found, run `notmuch'."
+If the current buffer is the only notmuch buffer, bury it. If no
+notmuch buffers exist, run `notmuch'."
   (interactive)
-  (let ((last
-        (loop for buffer in (buffer-list)
-              if (with-current-buffer buffer
-                   (memq major-mode '(notmuch-show-mode
-                                      notmuch-search-mode
-                                      notmuch-hello-mode)))
-              return buffer)))
-    (if last
-       (switch-to-buffer last)
+
+  (let (start first)
+    ;; If the current buffer is a notmuch buffer, remember it and then
+    ;; bury it.
+    (when (notmuch-interesting-buffer (current-buffer))
+      (setq start (current-buffer))
+      (bury-buffer))
+
+    ;; Find the first notmuch buffer.
+    (setq first (loop for buffer in (buffer-list)
+                    if (notmuch-interesting-buffer buffer)
+                    return buffer))
+
+    (if first
+       ;; If the first one we found is any other than the starting
+       ;; buffer, switch to it.
+       (unless (eq first start)
+         (switch-to-buffer first))
       (notmuch))))
 
 (setq mail-user-agent 'notmuch-user-agent)
index 8103bd96ef88c0e30f256753145f17f5583118a2..c928d02bb147b039dae529be337dda8055a0b757 100644 (file)
@@ -582,15 +582,15 @@ notmuch_database_t *
 notmuch_database_open (const char *path,
                       notmuch_database_mode_t mode)
 {
+    void *local = talloc_new (NULL);
     notmuch_database_t *notmuch = NULL;
-    char *notmuch_path = NULL, *xapian_path = NULL;
+    char *notmuch_path, *xapian_path;
     struct stat st;
     int err;
     unsigned int i, version;
     static int initialized = 0;
 
-    if (asprintf (&notmuch_path, "%s/%s", path, ".notmuch") == -1) {
-       notmuch_path = NULL;
+    if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
        fprintf (stderr, "Out of memory\n");
        goto DONE;
     }
@@ -602,8 +602,7 @@ notmuch_database_open (const char *path,
        goto DONE;
     }
 
-    if (asprintf (&xapian_path, "%s/%s", notmuch_path, "xapian") == -1) {
-       xapian_path = NULL;
+    if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
        fprintf (stderr, "Out of memory\n");
        goto DONE;
     }
@@ -617,7 +616,7 @@ notmuch_database_open (const char *path,
        initialized = 1;
     }
 
-    notmuch = talloc (NULL, notmuch_database_t);
+    notmuch = talloc_zero (NULL, notmuch_database_t);
     notmuch->exception_reported = FALSE;
     notmuch->path = talloc_strdup (notmuch, path);
 
@@ -703,14 +702,12 @@ notmuch_database_open (const char *path,
     } catch (const Xapian::Error &error) {
        fprintf (stderr, "A Xapian exception occurred opening database: %s\n",
                 error.get_msg().c_str());
+       notmuch_database_close (notmuch);
        notmuch = NULL;
     }
 
   DONE:
-    if (notmuch_path)
-       free (notmuch_path);
-    if (xapian_path)
-       free (xapian_path);
+    talloc_free (local);
 
     return notmuch;
 }
index e8e9922b408a4d59a771aedf6783fc73126b0ad0..d8f8b2bf516247ccc14aec8927dc5ae703a830d3 100644 (file)
@@ -339,6 +339,10 @@ _index_mime_part (notmuch_message_t *message,
                if (i > 1)
                    fprintf (stderr, "Warning: Unexpected extra parts of multipart/signed. Indexing anyway.\n");
            }
+           if (GMIME_IS_MULTIPART_ENCRYPTED (multipart)) {
+               /* Don't index encrypted parts. */
+               continue;
+           }
            _index_mime_part (message,
                              g_mime_multipart_get_part (multipart, i));
        }
index 7bcd1abfb4ce16791c6306c444215e1ae25418be..11218648f8c15f86f5a5dd6c5c0ece696577e46a 100644 (file)
@@ -127,8 +127,10 @@ notmuch_messages_get (notmuch_messages_t *messages)
 void
 notmuch_messages_move_to_next (notmuch_messages_t *messages)
 {
-    if (! messages->is_of_list_type)
-       return _notmuch_mset_messages_move_to_next (messages);
+    if (! messages->is_of_list_type) {
+       _notmuch_mset_messages_move_to_next (messages);
+       return;
+    }
 
     if (messages->iterator == NULL)
        return;
index 60a932fc936324fc39fedb3b736d86aeaa4857e5..7bf153e03fcd1b35bcc697f505e9d6441ecef9c4 100644 (file)
@@ -458,7 +458,7 @@ typedef struct _notmuch_string_node {
     struct _notmuch_string_node *next;
 } notmuch_string_node_t;
 
-typedef struct _notmuch_string_list {
+typedef struct visible _notmuch_string_list {
     int length;
     notmuch_string_node_t *head;
     notmuch_string_node_t **tail;
index 9f23a106e53b7acef19d1bf26036ed6993a361df..7929fe7222ea0bc92d0842a2444d92373a0fc4c9 100644 (file)
@@ -457,6 +457,12 @@ notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
 notmuch_sort_t
 notmuch_query_get_sort (notmuch_query_t *query);
 
+/* Add a tag that will be excluded from the query results by default.
+ * This exclusion will be overridden if this tag appears explicitly in
+ * the query. */
+void
+notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
+
 /* Execute a query for threads, returning a notmuch_threads_t object
  * which can be used to iterate over the results. The returned threads
  * object is owned by the query and as such, will only be valid until
index b6c0f12d9cd87f73993be6ef6c822319df888995..0b366025ba847314605157e23b193410a792e801 100644 (file)
@@ -27,6 +27,7 @@ struct _notmuch_query {
     notmuch_database_t *notmuch;
     const char *query_string;
     notmuch_sort_t sort;
+    notmuch_string_list_t *exclude_terms;
 };
 
 typedef struct _notmuch_mset_messages {
@@ -76,6 +77,8 @@ notmuch_query_create (notmuch_database_t *notmuch,
 
     query->sort = NOTMUCH_SORT_NEWEST_FIRST;
 
+    query->exclude_terms = _notmuch_string_list_create (query);
+
     return query;
 }
 
@@ -97,6 +100,13 @@ notmuch_query_get_sort (notmuch_query_t *query)
     return query->sort;
 }
 
+void
+notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag)
+{
+    char *term = talloc_asprintf (query, "%s%s", _find_prefix ("tag"), tag);
+    _notmuch_string_list_append (query->exclude_terms, term);
+}
+
 /* We end up having to call the destructors 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
@@ -112,6 +122,27 @@ _notmuch_messages_destructor (notmuch_mset_messages_t *messages)
     return 0;
 }
 
+/* Return a query that does not match messages with the excluded tags
+ * registered with the query.  Any tags that explicitly appear in
+ * xquery will not be excluded. */
+static Xapian::Query
+_notmuch_exclude_tags (notmuch_query_t *query, Xapian::Query xquery)
+{
+    for (notmuch_string_node_t *term = query->exclude_terms->head; term;
+        term = term->next) {
+       Xapian::TermIterator it = xquery.get_terms_begin ();
+       Xapian::TermIterator end = xquery.get_terms_end ();
+       for (; it != end; it++) {
+           if ((*it).compare (term->string) == 0)
+               break;
+       }
+       if (it == end)
+           xquery = Xapian::Query (Xapian::Query::OP_AND_NOT,
+                                   xquery, Xapian::Query (term->string));
+    }
+    return xquery;
+}
+
 notmuch_messages_t *
 notmuch_query_search_messages (notmuch_query_t *query)
 {
@@ -157,6 +188,8 @@ notmuch_query_search_messages (notmuch_query_t *query)
                                         mail_query, string_query);
        }
 
+       final_query = _notmuch_exclude_tags (query, final_query);
+
        enquire.set_weighting_scheme (Xapian::BoolWeight());
 
        switch (query->sort) {
@@ -436,6 +469,8 @@ notmuch_query_count_messages (notmuch_query_t *query)
                                         mail_query, string_query);
        }
 
+       final_query = _notmuch_exclude_tags (query, final_query);
+
        enquire.set_weighting_scheme(Xapian::BoolWeight());
        enquire.set_docid_order(Xapian::Enquire::ASCENDING);
 
diff --git a/man/.gitignore b/man/.gitignore
new file mode 100644 (file)
index 0000000..26ead20
--- /dev/null
@@ -0,0 +1,2 @@
+# ignore gzipped man pages
+*.[0-9].gz
diff --git a/man/Makefile b/man/Makefile
new file mode 100644 (file)
index 0000000..fa25832
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/man/Makefile.local b/man/Makefile.local
new file mode 100644 (file)
index 0000000..d43a949
--- /dev/null
@@ -0,0 +1,60 @@
+# -*- Makefile -*-
+
+dir := man
+
+# this variable seems to be needed to prevent lazy evaluation causing
+# problems with $(dir) changing values.
+MAIN_PAGE := $(dir)/man1/notmuch.1
+
+MAN1 := \
+       $(MAIN_PAGE) \
+       $(dir)/man1/notmuch-config.1 \
+       $(dir)/man1/notmuch-count.1 \
+       $(dir)/man1/notmuch-dump.1 \
+       $(dir)/man1/notmuch-restore.1 \
+       $(dir)/man1/notmuch-new.1 \
+       $(dir)/man1/notmuch-reply.1 \
+       $(dir)/man1/notmuch-search.1 \
+       $(dir)/man1/notmuch-show.1 \
+       $(dir)/man1/notmuch-tag.1
+
+MAN5 := $(dir)/man5/notmuch-hooks.5
+MAN7 := $(dir)/man7/notmuch-search-terms.7
+
+MAN1_GZ := $(addsuffix .gz,$(MAN1))
+MAN5_GZ := $(addsuffix .gz,$(MAN5))
+MAN7_GZ := $(addsuffix .gz,$(MAN7))
+
+MAN_SOURCE := $(MAN1) $(MAN5) $(MAN7)
+MAN_BACKUP := $(addsuffix .bak,$(MAN_SOURCE))
+COMPRESSED_MAN := $(MAN1_GZ) $(MAN5_GZ) $(MAN7_GZ)
+
+%.gz: %
+       gzip --stdout $^ > $@
+
+.PHONY: install-man update-man-versions verify-version-manpage
+
+install-man: $(COMPRESSED_MAN)
+       mkdir -p "$(DESTDIR)$(mandir)/man1"
+       mkdir -p "$(DESTDIR)$(mandir)/man5"
+       mkdir -p "$(DESTDIR)$(mandir)/man7"
+       install -m0644 $(MAN1_GZ) $(DESTDIR)/$(mandir)/man1
+       install -m0644 $(MAN5_GZ) $(DESTDIR)/$(mandir)/man5
+       install -m0644 $(MAN7_GZ) $(DESTDIR)/$(mandir)/man7
+       cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch.1.gz notmuch-setup.1.gz
+
+verify-version-manpage: verify-version-components
+       @echo -n "Checking that manual page version is $(VERSION)..."
+       @[ "$(VERSION)" = $$(sed -n '/^[.]TH NOTMUCH 1/{s/.*"Notmuch //;s/".*//p;}' $(MAIN_PAGE)) ] || \
+               (echo "No." && \
+                echo "Please edit version and notmuch.1 to have consistent versions." && false)
+       @echo "Good."
+
+update-man-versions: $(MAN_SOURCE)
+       for file in $(MAN_SOURCE); do \
+           cp $$file $$file.bak ; \
+           sed "s/^.TH NOTMUCH\([^[:blank:]]*\) \([1-9]\) .*$$/.TH NOTMUCH\1 \2 ${DATE} \"Notmuch ${VERSION}\"/" \
+               < $$file.bak > $$file; \
+       done
+
+CLEAN := $(CLEAN) $(COMPRESSED_MAN) $(MAN_BACKUP)
diff --git a/man/man1/notmuch-config.1 b/man/man1/notmuch-config.1
new file mode 100644 (file)
index 0000000..cb3234f
--- /dev/null
@@ -0,0 +1,54 @@
+.TH NOTMUCH-CONFIG 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-config \- Output a single part of a multipart MIME message.
+.SH SYNOPSIS
+
+.B notmuch config get
+.RI  "<" section "> . <" item ">"
+
+.B notmuch config set
+.RI  "<" section "> . <" item "> [" value "]"
+
+.SH DESCRIPTION
+
+The
+.B config
+command can be used to get or set settings in the notmuch
+configuration file.
+
+.SS GET
+
+The value of the specified configuration item is printed to stdout. If
+the item has multiple values, each value is separated by a newline
+character.
+
+Available configuration items include at least
+
+       database.path
+
+       user.name
+
+       user.primary_email
+
+       user.other_email
+
+       new.tags
+
+.SS SET
+
+The specified configuration item is set to the given value.  To
+specify a multiple-value item, provide each value as a separate
+command-line argument.
+
+If no values are provided, the specified configuration item will be
+removed from the configuration file.
+.RE
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(1), \fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1),
+\fBnotmuch-part\fR(1), \fBnotmuch-reply\fR(1),
+\fBnotmuch-restore\fR(1), \fBnotmuch-search\fR(1),
+\fBnotmuch-search-terms\fR(7), \fBnotmuch-show\fR(1),
+\fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-count.1 b/man/man1/notmuch-count.1
new file mode 100644 (file)
index 0000000..25fe329
--- /dev/null
@@ -0,0 +1,50 @@
+.TH NOTMUCH-COUNT 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-count \- Count messages matching the given search terms.
+.SH SYNOPSIS
+
+.B notmuch count
+.RI  [ options "... ] <" search-term ">..."
+
+.SH DESCRIPTION
+
+Count messages matching the search terms.
+
+The number of matching messages (or threads) is output to stdout.
+
+With no search terms, a count of all messages (or threads) in the database will
+be displayed.
+
+See \fBnotmuch-search-terms\fR(7)
+for details of the supported syntax for <search-terms>.
+
+Supported options for
+.B count
+include
+.RS 4
+.TP 4
+.B \-\-output=(messages|threads)
+
+.RS 4
+.TP 4
+.B messages
+
+Output the number of matching messages. This is the default.
+.RE
+.RS 4
+.TP 4
+.B threads
+
+Output the number of matching threads.
+.RE
+.RE
+.RE
+.RE
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-dump\fR(1),
+\fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1), \fBnotmuch-part\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-restore\fR(1),
+\fBnotmuch-search\fR(1), \fBnotmuch-search-terms\fR(7),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-dump.1 b/man/man1/notmuch-dump.1
new file mode 100644 (file)
index 0000000..9ccf35d
--- /dev/null
@@ -0,0 +1,37 @@
+.TH NOTMUCH-DUMP 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-dump \- Creates a plain-text dump of the tags of each message.
+
+.SH SYNOPSIS
+
+.B "notmuch dump"
+.RI "[ <" filename "> ] [--]"
+.RI "[ <" search-term ">...]"
+
+.SH DESCRIPTION
+
+Dump tags for messages matching the given search terms.
+
+Output is to the given filename, if any, or to stdout.  Note that
+using the filename argument is deprecated.
+
+These tags are the only data in the notmuch database that can't be
+recreated from the messages themselves.  The output of notmuch dump is
+therefore the only critical thing to backup (and much more friendly to
+incremental backup than the native database files.)
+
+With no search terms, a dump of all messages in the database will be
+generated.  A "--" argument instructs notmuch that the
+remaining arguments are search terms.
+
+See \fBnotmuch-search-terms\fR(7)
+for details of the supported syntax for <search-terms>.
+
+.RE
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1), \fBnotmuch-part\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-restore\fR(1),
+\fBnotmuch-search\fR(1), \fBnotmuch-search-terms\fR(7),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-new.1 b/man/man1/notmuch-new.1
new file mode 100644 (file)
index 0000000..77d4776
--- /dev/null
@@ -0,0 +1,65 @@
+.TH NOTMUCH-NEW 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-new \- Incorporate new mail into the notmuch database.
+.SH SYNOPSIS
+
+.B notmuch new
+.RB "[" --no-hooks "]"
+
+.SH DESCRIPTION
+
+Find and import any new messages to the database.
+
+The
+.B new
+command scans all sub-directories of the database, performing
+full-text indexing on new messages that are found. Each new message
+will automatically be tagged with both the
+.BR inbox " and " unread
+tags.
+
+You should run
+.B "notmuch new"
+once after first running
+.B "notmuch setup"
+to create the initial database. The first run may take a long time if
+you have a significant amount of mail (several hundred thousand
+messages or more). Subsequently, you should run
+.B "notmuch new"
+whenever new mail is delivered and you wish to incorporate it into the
+database. These subsequent runs will be much quicker than the initial
+run.
+
+Invoking
+.B notmuch
+with no command argument will run
+.B new
+if
+.B "notmuch setup"
+has previously been completed, but
+.B "notmuch new"
+has not previously been run.
+
+
+The
+.B new
+command supports hooks. See  \fBnotmuch-hooks(5)\fR
+for more details on hooks.
+
+Supported options for
+.B new
+include
+.RS 4
+.TP 4
+.BR \-\-no\-hooks
+
+Prevents hooks from being run.
+.RE
+.RE
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(5), \fBnotmuch-hooks\fR(5), \fBnotmuch-part\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-restore\fR(1),
+\fBnotmuch-search\fR(1), \fBnotmuch-search-terms\fR(7),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-reply.1 b/man/man1/notmuch-reply.1
new file mode 100644 (file)
index 0000000..5160ece
--- /dev/null
@@ -0,0 +1,86 @@
+.TH NOTMUCH-REPLY 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-reply \- Constructs a reply template for a set of messages.
+
+.SH SYNOPSIS
+
+.B notmuch reply
+.RI "[" options "...] <" search-term ">..."
+
+.SH DESCRIPTION
+
+Constructs a reply template for a set of messages.
+
+To make replying to email easier,
+.B notmuch reply
+takes an existing set of messages and constructs a suitable mail
+template. The Reply-to: header (if any, otherwise From:) is used for
+the To: address. Unless
+.BR \-\-reply-to=sender
+is specified, values from the To: and Cc: headers are copied, but not
+including any of the current user's email addresses (as configured in
+primary_mail or other_email in the .notmuch\-config file) in the
+recipient list.
+
+It also builds a suitable new subject, including Re: at the front (if
+not already present), and adding the message IDs of the messages being
+replied to to the References list and setting the In\-Reply\-To: field
+correctly.
+
+Finally, the original contents of the emails are quoted by prefixing
+each line with '> ' and included in the body.
+
+The resulting message template is output to stdout.
+
+Supported options for
+.B reply
+include
+.RS
+.TP 4
+.BR \-\-format= ( default | headers\-only )
+.RS
+.TP 4
+.BR default
+Includes subject and quoted message body.
+.TP
+.BR headers\-only
+Only produces In\-Reply\-To, References, To, Cc, and Bcc headers.
+.RE
+.RE
+.RS
+.TP 4
+.BR \-\-reply\-to= ( all | sender )
+.RS
+.TP 4
+.BR all " (default)"
+Replies to all addresses.
+.TP 4
+.BR sender
+Replies only to the sender. If replying to user's own message
+(Reply-to: or From: header is one of the user's configured email
+addresses), try To:, Cc:, and Bcc: headers in this order, and copy
+values from the first that contains something other than only the
+user's addresses.
+.RE
+.RE
+
+See \fBnotmuch-search-terms\fR(7)
+for details of the supported syntax for <search-terms>.
+
+Note: It is most common to use
+.B "notmuch reply"
+with a search string matching a single message, (such as
+id:<message-id>), but it can be useful to reply to several messages at
+once. For example, when a series of patches are sent in a single
+thread, replying to the entire thread allows for the reply to comment
+on issue found in multiple patches.
+.RE
+.RE
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(5), \fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1),
+\fBnotmuch-part\fR(1), \fBnotmuch-restore\fR(1),
+\fBnotmuch-search\fR(1), \fBnotmuch-search-terms\fR(7),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-restore.1 b/man/man1/notmuch-restore.1
new file mode 100644 (file)
index 0000000..2191df0
--- /dev/null
@@ -0,0 +1,39 @@
+.TH NOTMUCH-RESTORE 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-restore \- Restores the tags from the given file (see notmuch dump).
+
+.SH SYNOPSIS
+
+.B "notmuch restore"
+.RB [ "--accumulate" ]
+.RI "[ <" filename "> ]"
+
+.SH DESCRIPTION
+
+Restores the tags from the given file (see
+.BR "notmuch dump" ")."
+
+The input is read from the given filename, if any, or from stdin.
+
+Note: The dump file format is specifically chosen to be
+compatible with the format of files produced by sup-dump.
+So if you've previously been using sup for mail, then the
+.B "notmuch restore"
+command provides you a way to import all of your tags (or labels as
+sup calls them).
+
+The --accumulate switch causes the union of the existing and new tags to be
+applied, instead of replacing each message's tags as they are read in from the
+dump file.
+
+See \fBnotmuch-search-terms\fR(7)
+for details of the supported syntax for <search-terms>.
+
+.RE
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1), \fBnotmuch-part\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-dump\fR(1),
+\fBnotmuch-search\fR(1), \fBnotmuch-search-terms\fR(7),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-search.1 b/man/man1/notmuch-search.1
new file mode 100644 (file)
index 0000000..0bc3f40
--- /dev/null
@@ -0,0 +1,121 @@
+.TH NOTMUCH-SEARCH 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-search \- Search for messages matching the given search terms.
+.SH SYNOPSIS
+
+.B notmuch search
+.RI  [  options "...] <" search-term ">..."
+
+.SH DESCRIPTION
+
+Search for messages matching the given search terms, and display as
+results the threads containing the matched messages.
+
+The output consists of one line per thread, giving a thread ID, the
+date of the newest (or oldest, depending on the sort option) matched
+message in the thread, the number of matched messages and total
+messages in the thread, the names of all participants in the thread,
+and the subject of the newest (or oldest) message.
+
+See \fBnotmuch-search-terms\fR(7)
+for details of the supported syntax for <search-terms>.
+
+Supported options for
+.B search
+include
+.RS 4
+.TP 4
+.BR \-\-format= ( json | text )
+
+Presents the results in either JSON or plain-text (default).
+.RE
+
+.RS 4
+.TP 4
+.B \-\-output=(summary|threads|messages|files|tags)
+
+.RS 4
+.TP 4
+.B summary
+
+Output a summary of each thread with any message matching the search
+terms. The summary includes the thread ID, date, the number of
+messages in the thread (both the number matched and the total number),
+the authors of the thread and the subject.
+.RE
+.RS 4
+.TP 4
+.B threads
+
+Output the thread IDs of all threads with any message matching the
+search terms, either one per line (\-\-format=text) or as a JSON array
+(\-\-format=json).
+.RE
+.RS 4
+.TP 4
+.B messages
+
+Output the message IDs of all messages matching the search terms,
+either one per line (\-\-format=text) or as a JSON array
+(\-\-format=json).
+.RE
+.RS 4
+.TP 4
+.B files
+
+Output the filenames of all messages matching the search terms, either
+one per line (\-\-format=text) or as a JSON array (\-\-format=json).
+.RE
+.RS 4
+.TP 4
+.B tags
+
+Output all tags that appear on any message matching the search terms,
+either one per line (\-\-format=text) or as a JSON array
+(\-\-format=json).
+.RE
+.RE
+
+.RS 4
+.TP 4
+.BR \-\-sort= ( newest\-first | oldest\-first )
+
+This option can be used to present results in either chronological order
+.RB ( oldest\-first )
+or reverse chronological order
+.RB ( newest\-first ).
+
+Note: The thread order will be distinct between these two options
+(beyond being simply reversed). When sorting by
+.B oldest\-first
+the threads will be sorted by the oldest message in each thread, but
+when sorting by
+.B newest\-first
+the threads will be sorted by the newest message in each thread.
+
+By default, results will be displayed in reverse chronological order,
+(that is, the newest results will be displayed first).
+.RE
+
+.RS 4
+.TP 4
+.BR \-\-offset=[\-]N
+
+Skip displaying the first N results. With the leading '\-', start at the Nth
+result from the end.
+.RE
+
+.RS 4
+.TP 4
+.BR \-\-limit=N
+
+Limit the number of displayed results to N.
+.RE
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(5), \fBnotmuch-hooks\fR(5), \fBnotmuch-part\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-reply\fR(1),
+\fBnotmuch-restore\fR(1), \fBnotmuch-search-terms\fR(7),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-setup.1 b/man/man1/notmuch-setup.1
new file mode 120000 (symlink)
index 0000000..5c78dc8
--- /dev/null
@@ -0,0 +1 @@
+notmuch.1
\ No newline at end of file
diff --git a/man/man1/notmuch-show.1 b/man/man1/notmuch-show.1
new file mode 100644 (file)
index 0000000..b2301d8
--- /dev/null
@@ -0,0 +1,145 @@
+.TH NOTMUCH-SHOW 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-show \- Show messages matching the given search terms.
+.SH SYNOPSIS
+
+.B notmuch show
+.RI "[" options "...] <" search-term ">..."
+
+.SH DESCRIPTION
+
+Shows all messages matching the search terms.
+
+See \fBnotmuch-search-terms\fR(7)
+for details of the supported syntax for <search-terms>.
+
+The messages will be grouped and sorted based on the threading (all
+replies to a particular message will appear immediately after that
+message in date order). The output is not indented by default, but
+depth tags are printed so that proper indentation can be performed by
+a post-processor (such as the emacs interface to notmuch).
+
+Supported options for
+.B show
+include
+.RS 4
+.TP 4
+.B \-\-entire\-thread
+
+By default only those messages that match the search terms will be
+displayed. With this option, all messages in the same thread as any
+matched message will be displayed.
+.RE
+
+.RS 4
+.TP 4
+.B \-\-format=(text|json|mbox|raw)
+
+.RS 4
+.TP 4
+.BR text " (default for messages)"
+
+The default plain-text format has all 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
+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. For a multipart MIME message, these parts will
+be nested.
+.RE
+.RS 4
+.TP 4
+.B json
+
+The output is formatted with Javascript Object Notation (JSON). This
+format is more robust than the text format for automated
+processing. The nested structure of multipart MIME messages is
+reflected in nested JSON output. JSON output always includes all
+messages in a matching thread; in effect
+.B \-\-format=json
+implies
+.B \-\-entire\-thread
+
+.RE
+.RS 4
+.TP 4
+.B mbox
+
+All matching messages are output in the traditional, Unix mbox format
+with each message being prefixed by a line beginning with "From " and
+a blank line separating each message. Lines in the message content
+beginning with "From " (preceded by zero or more '>' characters) have
+an additional '>' character added. This reversible escaping
+is termed "mboxrd" format and described in detail here:
+
+.nf
+.nh
+http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html
+.hy
+.fi
+.
+.RE
+.RS 4
+.TP 4
+.BR raw " (default for a single part, see \-\-part)"
+
+For a message, the original, raw content of the email message is
+output. Consumers of this format should expect to implement MIME
+decoding and similar functions.
+
+For a single part (\-\-part) the raw part content is output after
+performing any necessary MIME decoding.
+
+The raw format must only be used with search terms matching single
+message.
+.RE
+.RE
+
+.RS 4
+.TP 4
+.B \-\-part=N
+
+Output the single decoded MIME part N of a single message.  The search
+terms must match only a single message.  Message parts are numbered in
+a depth-first walk of the message MIME structure, and are identified
+in the 'json' or 'text' output formats.
+.RE
+
+.RS 4
+.TP 4
+.B \-\-verify
+
+Compute and report the validity of any MIME cryptographic signatures
+found in the selected content (ie. "multipart/signed" parts). Status
+of the signature will be reported (currently only supported with
+--format=json), and the multipart/signed part will be replaced by the
+signed data.
+.RE
+
+.RS 4
+.TP 4
+.B \-\-decrypt
+
+Decrypt any MIME encrypted parts found in the selected content
+(ie. "multipart/encrypted" parts). Status of the decryption will be
+reported (currently only supported with --format=json) and the
+multipart/encrypted part will be replaced by the decrypted
+content.
+.RE
+
+A common use of
+.B notmuch show
+is to display a single thread of email messages. For this, use a
+search term of "thread:<thread-id>" as can be seen in the first
+column of output from the
+.B notmuch search
+command.
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(5), \fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1),
+\fBnotmuch-part\fR(1), \fBnotmuch-reply\fR(1),
+\fBnotmuch-restore\fR(1), \fBnotmuch-search\fR(1),
+\fBnotmuch-search-terms\fR(7), \fBnotmuch-tag\fR(1)
diff --git a/man/man1/notmuch-tag.1 b/man/man1/notmuch-tag.1
new file mode 100644 (file)
index 0000000..993911b
--- /dev/null
@@ -0,0 +1,32 @@
+.TH NOTMUCH-TAG 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch-tag \- Add/remove tags for all messages matching the search terms.
+
+.SH SYNOPSIS
+.B notmuch tag
+.RI  "+<" tag> "|\-<" tag "> [...] [\-\-] <" search-term ">..."
+
+.SH DESCRIPTION
+
+Add/remove tags for all messages matching the search terms.
+
+See \fBnotmuch-search-terms\fR(7)
+for details of the supported syntax for <search-terms>.
+
+Tags prefixed by '+' are added while those prefixed by '\-' are
+removed. For each message, tag removal is performed before tag
+addition.
+
+The beginning of <search-terms> is recognized by the first
+argument that begins with neither '+' nor '\-'. Support for
+an initial search term beginning with '+' or '\-' is provided
+by allowing the user to specify a "\-\-" argument to separate
+the tags from the search terms.
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(5), \fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1),
+\fBnotmuch-part\fR(1), \fBnotmuch-reply\fR(1),
+\fBnotmuch-restore\fR(1), \fBnotmuch-search\fR(1),
+\fBnotmuch-search-terms\fR(7), \fBnotmuch-show\fR(1)
diff --git a/man/man1/notmuch.1 b/man/man1/notmuch.1
new file mode 100644 (file)
index 0000000..424ca36
--- /dev/null
@@ -0,0 +1,148 @@
+.\" notmuch - Not much of an email program, (just index, search and tagging)
+.\"
+.\" Copyright © 2009 Carl Worth
+.\"
+.\" 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 this program.  If not, see http://www.gnu.org/licenses/ .
+.\"
+.\" Author: Carl Worth <cworth@cworth.org>
+.TH NOTMUCH 1 2011-12-04 "Notmuch 0.10.2"
+.SH NAME
+notmuch \- thread-based email index, search, and tagging
+.SH SYNOPSIS
+.B notmuch
+.IR command " [" args " ...]"
+.SH DESCRIPTION
+Notmuch is a command-line based program for indexing, searching,
+reading, and tagging large collections of email messages.
+
+This page describes how to get started using notmuch from the command
+line, and gives a brief overview of the commands available. For more
+information on e.g.
+.B notmuch show
+consult the \fBnotmuch-show\fR(1) man page, also accessible via
+.B notmuch help show
+
+The quickest way to get started with Notmuch is to simply invoke the
+.B notmuch
+command with no arguments, which will interactively guide you through
+the process of indexing your mail.
+.SH NOTE
+While the command-line program
+.B notmuch
+provides powerful functionality, it does not provide the most
+convenient interface for that functionality. More sophisticated
+interfaces are expected to be built on top of either the command-line
+interface, or more likely, on top of the notmuch library
+interface. See http://notmuchmail.org for more about alternate
+interfaces to notmuch. The emacs-based interface to notmuch (available under
+.B emacs/
+in the Notmuch source distribution) is probably the most widely used at
+this time.
+
+.SH COMMANDS
+
+
+.SS SETUP
+
+The
+.B notmuch setup
+command is used to configure Notmuch for first use, (or to reconfigure
+it later).
+
+The setup command will prompt for your full name, your primary email
+address, any alternate email addresses you use, and the directory
+containing your email archives. Your answers will be written to a
+configuration file in ${NOTMUCH_CONFIG} (if set) or
+${HOME}/.notmuch-config . This configuration file will be created with
+descriptive comments, making it easy to edit by hand later to change the
+configuration. Or you can run
+.B "notmuch setup"
+again to change the configuration.
+
+The mail directory you specify can contain any number of
+sub-directories and should primarily contain only files with individual
+email messages (eg. maildir or mh archives are perfect). If there are
+other, non-email files (such as indexes maintained by other email
+programs) then notmuch will do its best to detect those and ignore
+them.
+
+Mail storage that uses mbox format, (where one mbox file contains many
+messages), will not work with notmuch. If that's how your mail is
+currently stored, it is recommended you first convert it to maildir
+format with a utility such as mb2md before running
+.B "notmuch setup" .
+
+Invoking
+.B notmuch
+with no command argument will run
+.B setup
+if the setup command has not previously been completed.
+.RE
+
+.SS OTHER COMMANDS
+
+Several of the notmuch commands accept search terms with a common
+syntax. See \fNnotmuch-search-terms\fR(7)
+for more details on the supported syntax.
+
+The
+.BR search ", " show " and " count
+commands are used to query the email database.
+
+The
+.B reply
+command is useful for preparing a template for an email reply.
+
+The
+.B tag
+command is the only command available for manipulating database
+contents.
+
+
+The
+.BR dump " and " restore
+commands can be used to create a textual dump of email tags for backup
+purposes, and to restore from that dump.
+
+The
+.B config
+command can be used to get or set settings int the notmuch
+configuration file.
+
+.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
+
+\fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(1), \fBnotmuch-hooks\fR(5), \fBnotmuch-new\fR(1),
+\fBnotmuch-part\fR(1), \fBnotmuch-reply\fR(1),
+\fBnotmuch-restore\fR(1), \fBnotmuch-search\fR(1),
+\fBnotmuch-search-terms\fR(7), \fBnotmuch-show\fR(1),
+\fBnotmuch-tag\fR(1)
+
+
+The notmuch website:
+.B http://notmuchmail.org
+.SH CONTACT
+Feel free to send questions, comments, or kudos to the notmuch mailing
+list <notmuch@notmuchmail.org> . Subscription is not required before
+posting, but is available from the notmuchmail.org website.
+
+Real-time interaction with the Notmuch community is available via IRC
+(server: irc.freenode.net, channel: #notmuch).
diff --git a/man/man5/notmuch-hooks.5 b/man/man5/notmuch-hooks.5
new file mode 100644 (file)
index 0000000..2c4e552
--- /dev/null
@@ -0,0 +1,48 @@
+.TH NOTMUCH-HOOKS 5 2011-12-04 "Notmuch 0.10.2"
+
+.SH NAME
+notmuch-hooks \- hooks for notmuch
+
+.SH SYNOPSIS
+ $DATABASEDIR/.notmuch/hooks/*
+
+.SH DESCRIPTION
+Hooks are scripts (or arbitrary executables or symlinks to such) that notmuch
+invokes before and after certain actions. These scripts reside in
+the .notmuch/hooks directory within the database directory and must have
+executable permissions.
+
+The currently available hooks are described below.
+.RS 4
+.TP 4
+.B pre\-new
+This hook is invoked by the
+.B new
+command before scanning or importing new messages into the database. If this
+hook exits with a non-zero status, notmuch will abort further processing of the
+.B new
+command.
+
+Typically this hook is used for fetching or delivering new mail to be imported
+into the database.
+.RE
+.RS 4
+.TP 4
+.B post\-new
+This hook is invoked by the
+.B new
+command after new messages have been imported into the database and initial tags
+have been applied. The hook will not be run if there have been any errors during
+the scan or import.
+
+Typically this hook is used to perform additional query\-based tagging on the
+imported messages.
+.RE
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(5), \fBnotmuch-new\fR(1), \fBnotmuch-part\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-restore\fR(1),
+\fBnotmuch-search\fR(1), \fBnotmuch-search-terms\fR(7),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/man/man7/notmuch-search-terms.7 b/man/man7/notmuch-search-terms.7
new file mode 100644 (file)
index 0000000..a53565b
--- /dev/null
@@ -0,0 +1,141 @@
+.TH NOTMUCH-SEARCH-TERMS 7 2011-12-04 "Notmuch 0.10.2"
+
+.SH NAME
+notmuch-search-terms \- Syntax for notmuch queries
+
+.SH SYNOPSIS
+
+.B notmuch count
+.RI  [ options... ]
+.RI  < search-term ">..."
+
+.B "notmuch dump"
+.RI "[ <" filename "> ] [--]"
+.RI "[ <" search-term ">...]"
+
+.B notmuch search
+.RI  [  options "...] <" search-term ">..."
+
+.B notmuch show
+.RI "[" options "...] <" search-term ">..."
+
+.B notmuch tag
+.RI  "+<" tag> "|\-<" tag "> [...] [\-\-] <" search-term ">..."
+
+
+.SH DESCRIPTION
+Several notmuch commands accept a common syntax for search terms.
+
+The search terms can consist of free-form text (and quoted phrases)
+which will match all messages that contain all of the given
+terms/phrases in the body, the subject, or any of the sender or
+recipient headers.
+
+As a special case, a search string consisting of exactly a single
+asterisk ("*") will match all messages.
+
+In addition to free text, the following prefixes can be used to force
+terms to match against specific portions of an email, (where
+<brackets> indicate user-supplied values):
+
+       from:<name-or-address>
+
+       to:<name-or-address>
+
+       subject:<word-or-quoted-phrase>
+
+       attachment:<word>
+
+       tag:<tag> (or is:<tag>)
+
+       id:<message-id>
+
+       thread:<thread-id>
+
+       folder:<directory-path>
+
+The
+.B from:
+prefix is used to match the name or address of the sender of an email
+message.
+
+The
+.B to:
+prefix is used to match the names or addresses of any recipient of an
+email message, (whether To, Cc, or Bcc).
+
+Any term prefixed with
+.B subject:
+will match only text from the subject of an email. Searching for a
+phrase in the subject is supported by including quotation marks around
+the phrase, immediately following
+.BR subject: .
+
+The
+.B attachment:
+prefix can be used to search for specific filenames (or extensions) of
+attachments to email messages.
+
+For
+.BR tag: " and " is:
+valid tag values include
+.BR inbox " and " unread
+by default for new messages added by
+.B notmuch new
+as well as any other tag values added manually with
+.BR "notmuch tag" .
+
+For
+.BR id: ,
+message ID values are the literal contents of the Message\-ID: header
+of email messages, but without the '<', '>' delimiters.
+
+The
+.B thread:
+prefix can be used with the thread ID values that are generated
+internally by notmuch (and do not appear in email messages). These
+thread ID values can be seen in the first column of output from
+.B "notmuch search"
+
+The
+.B folder:
+prefix can be used to search for email message files that are
+contained within particular directories within the mail store. Only
+the directory components below the top-level mail database path are
+available to be searched.
+
+In addition to individual terms, multiple terms can be
+combined with Boolean operators (
+.BR and ", " or ", " not
+, etc.). Each term in the query will be implicitly connected by a
+logical AND if no explicit operator is provided, (except that terms
+with a common prefix will be implicitly combined with OR until we get
+Xapian defect #402 fixed).
+
+Parentheses can also be used to control the combination of the Boolean
+operators, but will have to be protected from interpretation by the
+shell, (such as by putting quotation marks around any parenthesized
+expression).
+
+Finally, results can be restricted to only messages within a
+particular time range, (based on the Date: header) with a syntax of:
+
+       <initial-timestamp>..<final-timestamp>
+
+Each timestamp is a number representing the number of seconds since
+1970\-01\-01 00:00:00 UTC. This is not the most convenient means of
+expressing date ranges, but until notmuch is fixed to accept a more
+convenient form, one can use the date program to construct
+timestamps. For example, with the bash shell the following syntax would
+specify a date range to return messages from 2009\-10\-01 until the
+current time:
+
+       $(date +%s \-d 2009\-10\-01)..$(date +%s)
+
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
+\fBnotmuch-dump\fR(5), \fBnotmuch-hooks\fR(5), \fBnotmuch-part\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-restore\fR(1),
+\fBnotmuch-search\fR(1), \fBnotmuch-search\fR(1),
+\fBnotmuch-show\fR(1), \fBnotmuch-tag\fR(1)
diff --git a/mime-node.c b/mime-node.c
new file mode 100644 (file)
index 0000000..d6b4506
--- /dev/null
@@ -0,0 +1,350 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ *          Keith Packard <keithp@keithp.com>
+ *          Austin Clements <aclements@csail.mit.edu>
+ */
+
+#include "notmuch-client.h"
+
+/* Context that gets inherited from the root node. */
+typedef struct mime_node_context {
+    /* Per-message resources.  These are allocated internally and must
+     * be destroyed. */
+    FILE *file;
+    GMimeStream *stream;
+    GMimeParser *parser;
+    GMimeMessage *mime_message;
+
+    /* Context provided by the caller. */
+#ifdef GMIME_ATLEAST_26
+    GMimeCryptoContext *cryptoctx;
+#else
+    GMimeCipherContext *cryptoctx;
+#endif
+    notmuch_bool_t decrypt;
+} mime_node_context_t;
+
+static int
+_mime_node_context_free (mime_node_context_t *res)
+{
+    if (res->mime_message)
+       g_object_unref (res->mime_message);
+
+    if (res->parser)
+       g_object_unref (res->parser);
+
+    if (res->stream)
+       g_object_unref (res->stream);
+
+    if (res->file)
+       fclose (res->file);
+
+    return 0;
+}
+
+notmuch_status_t
+mime_node_open (const void *ctx, notmuch_message_t *message,
+#ifdef GMIME_ATLEAST_26
+               GMimeCryptoContext *cryptoctx,
+#else
+               GMimeCipherContext *cryptoctx,
+#endif
+               notmuch_bool_t decrypt, mime_node_t **root_out)
+{
+    const char *filename = notmuch_message_get_filename (message);
+    mime_node_context_t *mctx;
+    mime_node_t *root;
+    notmuch_status_t status;
+
+    root = talloc_zero (ctx, mime_node_t);
+    if (root == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    /* Create the tree-wide context */
+    mctx = talloc_zero (root, mime_node_context_t);
+    if (mctx == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+    talloc_set_destructor (mctx, _mime_node_context_free);
+
+    mctx->file = fopen (filename, "r");
+    if (! mctx->file) {
+       fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+       status = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+
+    mctx->stream = g_mime_stream_file_new (mctx->file);
+    g_mime_stream_file_set_owner (GMIME_STREAM_FILE (mctx->stream), FALSE);
+
+    mctx->parser = g_mime_parser_new_with_stream (mctx->stream);
+
+    mctx->mime_message = g_mime_parser_construct_message (mctx->parser);
+
+    mctx->cryptoctx = cryptoctx;
+    mctx->decrypt = decrypt;
+
+    /* Create the root node */
+    root->part = GMIME_OBJECT (mctx->mime_message);
+    root->envelope_file = message;
+    root->nchildren = 1;
+    root->ctx = mctx;
+
+    root->parent = NULL;
+    root->part_num = 0;
+    root->next_child = 0;
+    root->next_part_num = 1;
+
+    *root_out = root;
+    return NOTMUCH_STATUS_SUCCESS;
+
+DONE:
+    talloc_free (root);
+    return status;
+}
+
+#ifdef GMIME_ATLEAST_26
+static int
+_signature_list_free (GMimeSignatureList **proxy)
+{
+    g_object_unref (*proxy);
+    return 0;
+}
+#else
+static int
+_signature_validity_free (GMimeSignatureValidity **proxy)
+{
+    g_mime_signature_validity_free (*proxy);
+    return 0;
+}
+#endif
+
+static mime_node_t *
+_mime_node_create (mime_node_t *parent, GMimeObject *part)
+{
+    mime_node_t *node = talloc_zero (parent, mime_node_t);
+    GError *err = NULL;
+
+    /* Set basic node properties */
+    node->part = part;
+    node->ctx = parent->ctx;
+    if (!talloc_reference (node, node->ctx)) {
+       fprintf (stderr, "Out of memory.\n");
+       talloc_free (node);
+       return NULL;
+    }
+    node->parent = parent;
+    node->part_num = node->next_part_num = -1;
+    node->next_child = 0;
+
+    /* Deal with the different types of parts */
+    if (GMIME_IS_PART (part)) {
+       node->nchildren = 0;
+    } else if (GMIME_IS_MULTIPART (part)) {
+       node->nchildren = g_mime_multipart_get_count (GMIME_MULTIPART (part));
+    } else if (GMIME_IS_MESSAGE_PART (part)) {
+       /* Promote part to an envelope and open it */
+       GMimeMessagePart *message_part = GMIME_MESSAGE_PART (part);
+       GMimeMessage *message = g_mime_message_part_get_message (message_part);
+       node->envelope_part = message_part;
+       node->part = GMIME_OBJECT (message);
+       node->nchildren = 1;
+    } else {
+       fprintf (stderr, "Warning: Unknown mime part type: %s.\n",
+                g_type_name (G_OBJECT_TYPE (part)));
+       talloc_free (node);
+       return NULL;
+    }
+
+    /* Handle PGP/MIME parts */
+    if (GMIME_IS_MULTIPART_ENCRYPTED (part)
+       && node->ctx->cryptoctx && node->ctx->decrypt) {
+       if (node->nchildren != 2) {
+           /* this violates RFC 3156 section 4, so we won't bother with it. */
+           fprintf (stderr, "Error: %d part(s) for a multipart/encrypted "
+                    "message (must be exactly 2)\n",
+                    node->nchildren);
+       } else {
+           GMimeMultipartEncrypted *encrypteddata =
+               GMIME_MULTIPART_ENCRYPTED (part);
+           node->decrypt_attempted = TRUE;
+#ifdef GMIME_ATLEAST_26
+           GMimeDecryptResult *decrypt_result = NULL;
+           node->decrypted_child = g_mime_multipart_encrypted_decrypt
+               (encrypteddata, node->ctx->cryptoctx, &decrypt_result, &err);
+#else
+           node->decrypted_child = g_mime_multipart_encrypted_decrypt
+               (encrypteddata, node->ctx->cryptoctx, &err);
+#endif
+           if (node->decrypted_child) {
+               node->decrypt_success = node->verify_attempted = TRUE;
+#ifdef GMIME_ATLEAST_26
+               /* This may be NULL if the part is not signed. */
+               node->sig_list = g_mime_decrypt_result_get_signatures (decrypt_result);
+               if (node->sig_list)
+                   g_object_ref (node->sig_list);
+               g_object_unref (decrypt_result);
+#else
+               node->sig_validity = g_mime_multipart_encrypted_get_signature_validity (encrypteddata);
+#endif
+           } else {
+               fprintf (stderr, "Failed to decrypt part: %s\n",
+                        (err ? err->message : "no error explanation given"));
+           }
+       }
+    } else if (GMIME_IS_MULTIPART_SIGNED (part) && node->ctx->cryptoctx) {
+       if (node->nchildren != 2) {
+           /* this violates RFC 3156 section 5, so we won't bother with it. */
+           fprintf (stderr, "Error: %d part(s) for a multipart/signed message "
+                    "(must be exactly 2)\n",
+                    node->nchildren);
+       } else {
+#ifdef GMIME_ATLEAST_26
+           node->sig_list = g_mime_multipart_signed_verify
+               (GMIME_MULTIPART_SIGNED (part), node->ctx->cryptoctx, &err);
+           node->verify_attempted = TRUE;
+
+           if (!node->sig_list)
+               fprintf (stderr, "Failed to verify signed part: %s\n",
+                        (err ? err->message : "no error explanation given"));
+#else
+           /* For some reason the GMimeSignatureValidity returned
+            * here is not a const (inconsistent with that
+            * returned by
+            * g_mime_multipart_encrypted_get_signature_validity,
+            * and therefore needs to be properly disposed of.
+            *
+            * In GMime 2.6, they're both non-const, so we'll be able
+            * to clean up this asymmetry. */
+           GMimeSignatureValidity *sig_validity = g_mime_multipart_signed_verify
+               (GMIME_MULTIPART_SIGNED (part), node->ctx->cryptoctx, &err);
+           node->verify_attempted = TRUE;
+           node->sig_validity = sig_validity;
+           if (sig_validity) {
+               GMimeSignatureValidity **proxy =
+                   talloc (node, GMimeSignatureValidity *);
+               *proxy = sig_validity;
+               talloc_set_destructor (proxy, _signature_validity_free);
+           }
+#endif
+       }
+    }
+
+#ifdef GMIME_ATLEAST_26
+    /* sig_list may be created in both above cases, so we need to
+     * cleanly handle it here. */
+    if (node->sig_list) {
+       GMimeSignatureList **proxy = talloc (node, GMimeSignatureList *);
+       *proxy = node->sig_list;
+       talloc_set_destructor (proxy, _signature_list_free);
+    }
+#endif
+
+#ifndef GMIME_ATLEAST_26
+    if (node->verify_attempted && !node->sig_validity)
+       fprintf (stderr, "Failed to verify signed part: %s\n",
+                (err ? err->message : "no error explanation given"));
+#endif
+
+    if (err)
+       g_error_free (err);
+
+    return node;
+}
+
+mime_node_t *
+mime_node_child (mime_node_t *parent, int child)
+{
+    GMimeObject *sub;
+    mime_node_t *node;
+
+    if (!parent || child < 0 || child >= parent->nchildren)
+       return NULL;
+
+    if (GMIME_IS_MULTIPART (parent->part)) {
+       if (child == 1 && parent->decrypted_child)
+           sub = parent->decrypted_child;
+       else
+           sub = g_mime_multipart_get_part
+               (GMIME_MULTIPART (parent->part), child);
+    } else if (GMIME_IS_MESSAGE (parent->part)) {
+       sub = g_mime_message_get_mime_part (GMIME_MESSAGE (parent->part));
+    } else {
+       /* This should have been caught by message_part_create */
+       INTERNAL_ERROR ("Unexpected GMimeObject type: %s",
+                       g_type_name (G_OBJECT_TYPE (parent->part)));
+    }
+    node = _mime_node_create (parent, sub);
+
+    if (child == parent->next_child && parent->next_part_num != -1) {
+       /* We're traversing in depth-first order.  Record the child's
+        * depth-first numbering. */
+       node->part_num = parent->next_part_num;
+       node->next_part_num = node->part_num + 1;
+
+       /* Prepare the parent for its next depth-first child. */
+       parent->next_child++;
+       parent->next_part_num = -1;
+
+       if (node->nchildren == 0) {
+           /* We've reached a leaf, so find the parent that has more
+            * children and set it up to number its next child. */
+           mime_node_t *iter = node->parent;
+           while (iter && iter->next_child == iter->nchildren)
+               iter = iter->parent;
+           if (iter)
+               iter->next_part_num = node->part_num + 1;
+       }
+    }
+
+    return node;
+}
+
+static mime_node_t *
+_mime_node_seek_dfs_walk (mime_node_t *node, int *n)
+{
+    mime_node_t *ret = NULL;
+    int i;
+
+    if (*n == 0)
+       return node;
+
+    *n -= 1;
+    for (i = 0; i < node->nchildren && !ret; i++) {
+       mime_node_t *child = mime_node_child (node, i);
+       ret = _mime_node_seek_dfs_walk (child, n);
+       if (!ret)
+           talloc_free (child);
+    }
+    return ret;
+}
+
+mime_node_t *
+mime_node_seek_dfs (mime_node_t *node, int n)
+{
+    if (n < 0)
+       return NULL;
+    return _mime_node_seek_dfs_walk (node, &n);
+}
index c602e2e08c28fd2d2d9011c2eacb3f7dac8f1d75..e0eb594ad0394510ecdd20e0a04ee1119f247775 100644 (file)
 
 #include <gmime/gmime.h>
 
+/* GMIME_CHECK_VERSION in gmime 2.4 is not usable from the
+ * preprocessor (it calls a runtime function). But since
+ * GMIME_MAJOR_VERSION and friends were added in gmime 2.6, we can use
+ * these to check the version number. */
+#ifdef GMIME_MAJOR_VERSION
+#define GMIME_ATLEAST_26
+#endif
+
 #include "notmuch.h"
 
 /* This is separate from notmuch-private.h because we're trying to
 #define STRINGIFY(s) STRINGIFY_(s)
 #define STRINGIFY_(s) #s
 
+struct mime_node;
+struct notmuch_show_params;
+
 typedef struct notmuch_show_format {
     const char *message_set_start;
+    void (*part) (const void *ctx,
+                 struct mime_node *node, int indent,
+                 const struct notmuch_show_params *params);
     const char *message_start;
     void (*message) (const void *ctx,
                     notmuch_message_t *message,
@@ -69,7 +83,11 @@ typedef struct notmuch_show_format {
     void (*part_start) (GMimeObject *part,
                        int *part_count);
     void (*part_encstatus) (int status);
+#ifdef GMIME_ATLEAST_26
+    void (*part_sigstatus) (GMimeSignatureList* siglist);
+#else
     void (*part_sigstatus) (const GMimeSignatureValidity* validity);
+#endif
     void (*part_content) (GMimeObject *part);
     void (*part_end) (GMimeObject *part);
     const char *part_sep;
@@ -83,7 +101,11 @@ typedef struct notmuch_show_params {
     int entire_thread;
     int raw;
     int part;
+#ifdef GMIME_ATLEAST_26
+    GMimeCryptoContext* cryptoctx;
+#else
     GMimeCipherContext* cryptoctx;
+#endif
     int decrypt;
 } notmuch_show_params_t;
 
@@ -162,7 +184,7 @@ char *
 query_string_from_args (void *ctx, int argc, char *argv[]);
 
 notmuch_status_t
-show_message_body (const char *filename,
+show_message_body (notmuch_message_t *message,
                   const notmuch_show_format_t *format,
                   notmuch_show_params_t *params);
 
@@ -235,11 +257,129 @@ void
 notmuch_config_set_maildir_synchronize_flags (notmuch_config_t *config,
                                              notmuch_bool_t synchronize_flags);
 
+const char **
+notmuch_config_get_search_exclude_tags (notmuch_config_t *config, size_t *length);
+
+void
+notmuch_config_set_search_exclude_tags (notmuch_config_t *config,
+                                     const char *list[],
+                                     size_t length);
+
 int
 notmuch_run_hook (const char *db_path, const char *hook);
 
 notmuch_bool_t
 debugger_is_active (void);
 
+/* mime-node.c */
+
+/* mime_node_t represents a single node in a MIME tree.  A MIME tree
+ * abstracts the different ways of traversing different types of MIME
+ * parts, allowing a MIME message to be viewed as a generic tree of
+ * parts.  Message-type parts have one child, multipart-type parts
+ * have multiple children, and leaf parts have zero children.
+ */
+typedef struct mime_node {
+    /* The MIME object of this part.  This will be a GMimeMessage,
+     * GMimePart, GMimeMultipart, or a subclass of one of these.
+     *
+     * This will never be a GMimeMessagePart because GMimeMessagePart
+     * is structurally redundant with GMimeMessage.  If this part is a
+     * message (that is, 'part' is a GMimeMessage), then either
+     * envelope_file will be set to a notmuch_message_t (for top-level
+     * messages) or envelope_part will be set to a GMimeMessagePart
+     * (for embedded message parts).
+     */
+    GMimeObject *part;
+
+    /* If part is a GMimeMessage, these record the envelope of the
+     * message: either a notmuch_message_t representing a top-level
+     * message, or a GMimeMessagePart representing a MIME part
+     * containing a message.
+     */
+    notmuch_message_t *envelope_file;
+    GMimeMessagePart *envelope_part;
+
+    /* The number of children of this part. */
+    int nchildren;
+
+    /* The parent of this node or NULL if this is the root node. */
+    struct mime_node *parent;
+
+    /* The depth-first part number of this child if the MIME tree is
+     * being traversed in depth-first order, or -1 otherwise. */
+    int part_num;
+
+    /* True if decryption of this part was attempted. */
+    notmuch_bool_t decrypt_attempted;
+    /* True if decryption of this part's child succeeded.  In this
+     * case, the decrypted part is substituted for the second child of
+     * this part (which would usually be the encrypted data). */
+    notmuch_bool_t decrypt_success;
+
+    /* True if signature verification on this part was attempted. */
+    notmuch_bool_t verify_attempted;
+#ifdef GMIME_ATLEAST_26
+    /* The list of signatures for signed or encrypted containers. If
+     * there are no signatures, this will be NULL. */
+    GMimeSignatureList* sig_list;
+#else
+    /* For signed or encrypted containers, the validity of the
+     * signature.  May be NULL if signature verification failed.  If
+     * there are simply no signatures, this will be non-NULL with an
+     * empty signers list. */
+    const GMimeSignatureValidity *sig_validity;
+#endif
+
+    /* Internal: Context inherited from the root iterator. */
+    struct mime_node_context *ctx;
+
+    /* Internal: For successfully decrypted multipart parts, the
+     * decrypted part to substitute for the second child. */
+    GMimeObject *decrypted_child;
+
+    /* Internal: The next child for depth-first traversal and the part
+     * number to assign it (or -1 if unknown). */
+    int next_child;
+    int next_part_num;
+} mime_node_t;
+
+/* Construct a new MIME node pointing to the root message part of
+ * message.  If cryptoctx is non-NULL, it will be used to verify
+ * signatures on any child parts.  If decrypt is true, then cryptoctx
+ * will additionally be used to decrypt any encrypted child parts.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Root node is returned in *node_out.
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: Failed to open message file.
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ */
+notmuch_status_t
+mime_node_open (const void *ctx, notmuch_message_t *message,
+#ifdef GMIME_ATLEAST_26
+               GMimeCryptoContext *cryptoctx,
+#else
+               GMimeCipherContext *cryptoctx,
+#endif
+               notmuch_bool_t decrypt, mime_node_t **node_out);
+
+/* Return a new MIME node for the requested child part of parent.
+ * parent will be used as the talloc context for the returned child
+ * node.
+ *
+ * In case of any failure, this function returns NULL, (after printing
+ * an error message on stderr).
+ */
+mime_node_t *
+mime_node_child (mime_node_t *parent, int child);
+
+/* Return the nth child of node in a depth-first traversal.  If n is
+ * 0, returns node itself.  Returns NULL if there is no such part. */
+mime_node_t *
+mime_node_seek_dfs (mime_node_t *node, int n);
+
 #include "command-line-arguments.h"
 #endif
index d697138af10150bc03c7cd4bd3a1d3fe73bfe87f..a124e34cf2d8a1beff94704c7a6179026ff85743 100644 (file)
@@ -84,6 +84,16 @@ static const char maildir_config_comment[] =
     "\tand update tags, while the \"notmuch tag\" and \"notmuch restore\"\n"
     "\tcommands will notice tag changes and update flags in filenames\n";
 
+static const char search_config_comment[] =
+    " Search configuration\n"
+    "\n"
+    " The following option is supported here:\n"
+    "\n"
+    "\texclude_tags\n"
+    "\t\tA ;-separated list of tags that will be excluded from\n"
+    "\t\tsearch results by default.  Using an excluded tag in a\n"
+    "\t\tquery will override that exclusion.\n";
+
 struct _notmuch_config {
     char *filename;
     GKeyFile *key_file;
@@ -96,6 +106,8 @@ struct _notmuch_config {
     const char **new_tags;
     size_t new_tags_length;
     notmuch_bool_t maildir_synchronize_flags;
+    const char **search_exclude_tags;
+    size_t search_exclude_tags_length;
 };
 
 static int
@@ -221,6 +233,7 @@ notmuch_config_open (void *ctx,
     int file_had_new_group;
     int file_had_user_group;
     int file_had_maildir_group;
+    int file_had_search_group;
 
     if (is_new_ret)
        *is_new_ret = 0;
@@ -252,6 +265,8 @@ notmuch_config_open (void *ctx,
     config->new_tags = NULL;
     config->new_tags_length = 0;
     config->maildir_synchronize_flags = TRUE;
+    config->search_exclude_tags = NULL;
+    config->search_exclude_tags_length = 0;
 
     if (! g_key_file_load_from_file (config->key_file,
                                     config->filename,
@@ -295,6 +310,7 @@ notmuch_config_open (void *ctx,
     file_had_new_group = g_key_file_has_group (config->key_file, "new");
     file_had_user_group = g_key_file_has_group (config->key_file, "user");
     file_had_maildir_group = g_key_file_has_group (config->key_file, "maildir");
+    file_had_search_group = g_key_file_has_group (config->key_file, "search");
 
 
     if (notmuch_config_get_database_path (config) == NULL) {
@@ -345,6 +361,15 @@ notmuch_config_open (void *ctx,
        notmuch_config_set_new_tags (config, tags, 2);
     }
 
+    if (notmuch_config_get_search_exclude_tags (config, &tmp) == NULL) {
+       if (is_new) {
+           const char *tags[] = { "deleted", "spam" };
+           notmuch_config_set_search_exclude_tags (config, tags, 2);
+       } else {
+           notmuch_config_set_search_exclude_tags (config, NULL, 0);
+       }
+    }
+
     error = NULL;
     config->maildir_synchronize_flags =
        g_key_file_get_boolean (config->key_file,
@@ -387,6 +412,11 @@ notmuch_config_open (void *ctx,
                                maildir_config_comment, NULL);
     }
 
+    if (! file_had_search_group) {
+       g_key_file_set_comment (config->key_file, "search", NULL,
+                               search_config_comment, NULL);
+    }
+
     if (is_new_ret)
        *is_new_ret = is_new;
 
@@ -437,6 +467,48 @@ notmuch_config_save (notmuch_config_t *config)
     return 0;
 }
 
+static const char **
+_config_get_list (notmuch_config_t *config,
+                 const char *section, const char *key,
+                 const char ***outlist, size_t *list_length, size_t *ret_length)
+{
+    assert(outlist);
+
+    if (*outlist == NULL) {
+
+       char **inlist = g_key_file_get_string_list (config->key_file,
+                                            section, key, list_length, NULL);
+       if (inlist) {
+           unsigned int i;
+
+           *outlist = talloc_size (config, sizeof (char *) * (*list_length + 1));
+
+           for (i = 0; i < *list_length; i++)
+               (*outlist)[i] = talloc_strdup (*outlist, inlist[i]);
+
+           (*outlist)[i] = NULL;
+
+           g_strfreev (inlist);
+       }
+    }
+
+    if (ret_length)
+       *ret_length = *list_length;
+
+    return *outlist;
+}
+
+static void
+_config_set_list (notmuch_config_t *config,
+                 const char *group, const char *name,
+                 const char *list[],
+                 size_t length, const char ***config_var )
+{
+    g_key_file_set_string_list (config->key_file, group, name, list, length);
+    talloc_free (*config_var);
+    *config_var = NULL;
+}
+
 const char *
 notmuch_config_get_database_path (notmuch_config_t *config)
 {
@@ -521,37 +593,6 @@ notmuch_config_set_user_primary_email (notmuch_config_t *config,
     config->user_primary_email = NULL;
 }
 
-static const char **
-_config_get_list (notmuch_config_t *config,
-                 const char *section, const char *key,
-                 const char ***outlist, size_t *list_length, size_t *ret_length)
-{
-    assert(outlist);
-
-    if (*outlist == NULL) {
-
-       char **inlist = g_key_file_get_string_list (config->key_file,
-                                            section, key, list_length, NULL);
-       if (inlist) {
-           unsigned int i;
-
-           *outlist = talloc_size (config, sizeof (char *) * (*list_length + 1));
-
-           for (i = 0; i < *list_length; i++)
-               (*outlist)[i] = talloc_strdup (*outlist, inlist[i]);
-
-           (*outlist)[i] = NULL;
-
-           g_strfreev (inlist);
-       }
-    }
-
-    if (ret_length)
-       *ret_length = *list_length;
-
-    return *outlist;
-}
-
 const char **
 notmuch_config_get_user_other_email (notmuch_config_t *config,   size_t *length)
 {
@@ -568,17 +609,6 @@ notmuch_config_get_new_tags (notmuch_config_t *config,   size_t *length)
                             &(config->new_tags_length), length);
 }
 
-static void
-_config_set_list (notmuch_config_t *config,
-                 const char *group, const char *name,
-                 const char *list[],
-                 size_t length, const char ***config_var )
-{
-    g_key_file_set_string_list (config->key_file, group, name, list, length);
-    talloc_free (*config_var);
-    *config_var = NULL;
-}
-
 void
 notmuch_config_set_user_other_email (notmuch_config_t *config,
                                     const char *list[],
@@ -597,6 +627,23 @@ notmuch_config_set_new_tags (notmuch_config_t *config,
                     &(config->new_tags));
 }
 
+const char **
+notmuch_config_get_search_exclude_tags (notmuch_config_t *config, size_t *length)
+{
+    return _config_get_list (config, "search", "exclude_tags",
+                            &(config->search_exclude_tags),
+                            &(config->search_exclude_tags_length), length);
+}
+
+void
+notmuch_config_set_search_exclude_tags (notmuch_config_t *config,
+                                     const char *list[],
+                                     size_t length)
+{
+    _config_set_list (config, "search", "exclude_tags", list, length,
+                     &(config->search_exclude_tags));
+}
+
 /* Given a configuration item of the form <group>.<key> return the
  * component group and key. If any error occurs, print a message on
  * stderr and return 1. Otherwise, return 0.
index 20ce33426c7072cf3bf6688a07083d5c89b1f328..63459fb611f98d9a69507df7afdb919e61272563 100644 (file)
 
 #include "notmuch-client.h"
 
+enum {
+    OUTPUT_THREADS,
+    OUTPUT_MESSAGES,
+};
+
 int
 notmuch_count_command (void *ctx, int argc, char *argv[])
 {
@@ -28,34 +33,25 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
     notmuch_database_t *notmuch;
     notmuch_query_t *query;
     char *query_str;
-    int i;
-    notmuch_bool_t output_messages = TRUE;
+    int opt_index;
+    int output = OUTPUT_MESSAGES;
+    const char **search_exclude_tags;
+    size_t search_exclude_tags_length;
+    unsigned int i;
 
-    argc--; argv++; /* skip subcommand argument */
+    notmuch_opt_desc_t options[] = {
+       { NOTMUCH_OPT_KEYWORD, &output, "output", 'o',
+         (notmuch_keyword_t []){ { "threads", OUTPUT_THREADS },
+                                 { "messages", OUTPUT_MESSAGES },
+                                 { 0, 0 } } },
+       { 0, 0, 0, 0, 0 }
+    };
 
-    for (i = 0; i < argc && argv[i][0] == '-'; i++) {
-       if (strcmp (argv[i], "--") == 0) {
-           i++;
-           break;
-       }
-       if (STRNCMP_LITERAL (argv[i], "--output=") == 0) {
-           const char *opt = argv[i] + sizeof ("--output=") - 1;
-           if (strcmp (opt, "threads") == 0) {
-               output_messages = FALSE;
-           } else if (strcmp (opt, "messages") == 0) {
-               output_messages = TRUE;
-           } else {
-               fprintf (stderr, "Invalid value for --output: %s\n", opt);
-               return 1;
-           }
-       } else {
-           fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
-           return 1;
-       }
-    }
+    opt_index = parse_arguments (argc, argv, options, 1);
 
-    argc -= i;
-    argv += i;
+    if (opt_index < 0) {
+       return 1;
+    }
 
     config = notmuch_config_open (ctx, NULL, NULL);
     if (config == NULL)
@@ -66,7 +62,7 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
     if (notmuch == NULL)
        return 1;
 
-    query_str = query_string_from_args (ctx, argc, argv);
+    query_str = query_string_from_args (ctx, argc-opt_index, argv+opt_index);
     if (query_str == NULL) {
        fprintf (stderr, "Out of memory.\n");
        return 1;
@@ -82,10 +78,19 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
        return 1;
     }
 
-    if (output_messages)
+    search_exclude_tags = notmuch_config_get_search_exclude_tags
+       (config, &search_exclude_tags_length);
+    for (i = 0; i < search_exclude_tags_length; i++)
+       notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+
+    switch (output) {
+    case OUTPUT_MESSAGES:
        printf ("%u\n", notmuch_query_count_messages (query));
-    else
+       break;
+    case OUTPUT_THREADS:
        printf ("%u\n", notmuch_query_count_threads (query));
+       break;
+    }
 
     notmuch_query_destroy (query);
     notmuch_database_close (notmuch);
index 3512de727734ad48025f3f88972587fed91d838b..a569a54454560ef33cb3fdad41c484658f514f35 100644 (file)
@@ -67,7 +67,11 @@ handle_sigint (unused (int sig))
 {
     static char msg[] = "Stopping...         \n";
 
-    (void) write(2, msg, sizeof(msg)-1);
+    /* This write is "opportunistic", so it's okay to ignore the
+     * result.  It is not required for correctness, and if it does
+     * fail or produce a short write, we want to get out of the signal
+     * handler as quickly as possible, not retry it. */
+    IGNORE_RESULT (write (2, msg, sizeof(msg)-1));
     interrupted = 1;
 }
 
index 7242310a7ec5f376872ac065044cbfc0e7c1fedb..f55b1d22bbe2f1d5ed1603dafc3b7d2615715fa9 100644 (file)
@@ -31,7 +31,7 @@ static void
 reply_part_content (GMimeObject *part);
 
 static const notmuch_show_format_t format_reply = {
-    "",
+    "", NULL,
        "", NULL,
            "", NULL, reply_headers_message_part, ">\n",
            "",
@@ -168,22 +168,29 @@ address_is_users (const char *address, notmuch_config_t *config)
     return 0;
 }
 
-/* For each address in 'list' that is not configured as one of the
- * user's addresses in 'config', add that address to 'message' as an
- * address of 'type'.
+/* Scan addresses in 'list'.
  *
- * The first address encountered that *is* the user's address will be
- * returned, (otherwise NULL is returned).
+ * If 'message' is non-NULL, then for each address in 'list' that is
+ * not configured as one of the user's addresses in 'config', add that
+ * address to 'message' as an address of 'type'.
+ *
+ * If 'user_from' is non-NULL and *user_from is NULL, *user_from will
+ * be set to the first address encountered in 'list' that is the
+ * user's address.
+ *
+ * Return the number of addresses added to 'message'. (If 'message' is
+ * NULL, the function returns 0 by definition.)
  */
-static const char *
-add_recipients_for_address_list (GMimeMessage *message,
-                                notmuch_config_t *config,
-                                GMimeRecipientType type,
-                                InternetAddressList *list)
+static unsigned int
+scan_address_list (InternetAddressList *list,
+                  notmuch_config_t *config,
+                  GMimeMessage *message,
+                  GMimeRecipientType type,
+                  const char **user_from)
 {
     InternetAddress *address;
     int i;
-    const char *ret = NULL;
+    unsigned int n = 0;
 
     for (i = 0; i < internet_address_list_length (list); i++) {
        address = internet_address_list_get_address (list, i);
@@ -196,8 +203,7 @@ add_recipients_for_address_list (GMimeMessage *message,
            if (group_list == NULL)
                continue;
 
-           add_recipients_for_address_list (message, config,
-                                            type, group_list);
+           n += scan_address_list (group_list, config, message, type, user_from);
        } else {
            InternetAddressMailbox *mailbox;
            const char *name;
@@ -209,40 +215,41 @@ add_recipients_for_address_list (GMimeMessage *message,
            addr = internet_address_mailbox_get_addr (mailbox);
 
            if (address_is_users (addr, config)) {
-               if (ret == NULL)
-                   ret = addr;
-           } else {
+               if (user_from && *user_from == NULL)
+                   *user_from = addr;
+           } else if (message) {
                g_mime_message_add_recipient (message, type, name, addr);
+               n++;
            }
        }
     }
 
-    return ret;
+    return n;
 }
 
-/* For each address in 'recipients' that is not configured as one of
- * the user's addresses in 'config', add that address to 'message' as
- * an address of 'type'.
+/* Scan addresses in 'recipients'.
  *
- * The first address encountered that *is* the user's address will be
- * returned, (otherwise NULL is returned).
+ * See the documentation of scan_address_list() above. This function
+ * does exactly the same, but converts 'recipients' to an
+ * InternetAddressList first.
  */
-static const char *
-add_recipients_for_string (GMimeMessage *message,
-                          notmuch_config_t *config,
-                          GMimeRecipientType type,
-                          const char *recipients)
+static unsigned int
+scan_address_string (const char *recipients,
+                    notmuch_config_t *config,
+                    GMimeMessage *message,
+                    GMimeRecipientType type,
+                    const char **user_from)
 {
     InternetAddressList *list;
 
     if (recipients == NULL)
-       return NULL;
+       return 0;
 
     list = internet_address_list_parse_string (recipients);
     if (list == NULL)
-       return NULL;
+       return 0;
 
-    return add_recipients_for_address_list (message, config, type, list);
+    return scan_address_list (list, config, message, type, user_from);
 }
 
 /* Does the address in the Reply-To header of 'message' already appear
@@ -284,15 +291,23 @@ reply_to_header_is_redundant (notmuch_message_t *message)
     return 0;
 }
 
-/* Augments the recipients of reply from the headers of message.
+/* Augment the recipients of 'reply' from the "Reply-to:", "From:",
+ * "To:", "Cc:", and "Bcc:" headers of 'message'.
  *
- * If any of the user's addresses were found in these headers, the first
- * of these returned, otherwise NULL is returned.
+ * If 'reply_all' is true, use sender and all recipients, otherwise
+ * scan the headers for the first that contains something other than
+ * the user's addresses and add the recipients from this header
+ * (typically this would be reply-to-sender, but also handles reply to
+ * user's own message in a sensible way).
+ *
+ * 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)
+                            notmuch_message_t *message,
+                            notmuch_bool_t reply_all)
 {
     struct {
        const char *header;
@@ -306,6 +321,7 @@ add_recipients_from_message (GMimeMessage *reply,
     };
     const char *from_addr = NULL;
     unsigned int i;
+    unsigned int n = 0;
 
     /* Some mailing lists munge the Reply-To header despite it being A Bad
      * Thing, see http://www.unicom.com/pw/reply-to-harmful.html
@@ -324,7 +340,7 @@ add_recipients_from_message (GMimeMessage *reply,
     }
 
     for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
-       const char *addr, *recipients;
+       const char *recipients;
 
        recipients = notmuch_message_get_header (message,
                                                 reply_to_map[i].header);
@@ -332,11 +348,24 @@ add_recipients_from_message (GMimeMessage *reply,
            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;
+       n += scan_address_string (recipients, config, reply,
+                                 reply_to_map[i].recipient_type, &from_addr);
+
+       if (!reply_all && n) {
+           /* Stop adding new recipients in reply-to-sender mode if
+            * we have added some recipient(s) above.
+            *
+            * This also handles the case of user replying to his own
+            * message, where reply-to/from is not a recipient. In
+            * this case there may be more than one recipient even if
+            * not replying to all.
+            */
+           reply = NULL;
+
+           /* From address and some recipients are enough, bail out. */
+           if (from_addr)
+               break;
+       }
     }
 
     return from_addr;
@@ -480,7 +509,8 @@ static int
 notmuch_reply_format_default(void *ctx,
                             notmuch_config_t *config,
                             notmuch_query_t *query,
-                            notmuch_show_params_t *params)
+                            notmuch_show_params_t *params,
+                            notmuch_bool_t reply_all)
 {
     GMimeMessage *reply;
     notmuch_messages_t *messages;
@@ -509,7 +539,8 @@ notmuch_reply_format_default(void *ctx,
            g_mime_message_set_subject (reply, subject);
        }
 
-       from_addr = add_recipients_from_message (reply, config, message);
+       from_addr = add_recipients_from_message (reply, config, message,
+                                                reply_all);
 
        if (from_addr == NULL)
            from_addr = guess_from_received_header (config, message);
@@ -546,8 +577,7 @@ notmuch_reply_format_default(void *ctx,
                notmuch_message_get_header (message, "date"),
                notmuch_message_get_header (message, "from"));
 
-       show_message_body (notmuch_message_get_filename (message),
-                          format, params);
+       show_message_body (message, format, params);
 
        notmuch_message_destroy (message);
     }
@@ -559,7 +589,8 @@ static int
 notmuch_reply_format_headers_only(void *ctx,
                                  notmuch_config_t *config,
                                  notmuch_query_t *query,
-                                 unused (notmuch_show_params_t *params))
+                                 unused (notmuch_show_params_t *params),
+                                 notmuch_bool_t reply_all)
 {
     GMimeMessage *reply;
     notmuch_messages_t *messages;
@@ -599,7 +630,7 @@ notmuch_reply_format_headers_only(void *ctx,
        g_mime_object_set_header (GMIME_OBJECT (reply),
                                  "References", references);
 
-       (void)add_recipients_from_message (reply, config, message);
+       (void)add_recipients_from_message (reply, config, message, reply_all);
 
        reply_headers = g_mime_object_to_string (GMIME_OBJECT (reply));
        printf ("%s", reply_headers);
@@ -613,62 +644,73 @@ notmuch_reply_format_headers_only(void *ctx,
     return 0;
 }
 
+enum {
+    FORMAT_DEFAULT,
+    FORMAT_HEADERS_ONLY,
+};
+
 int
 notmuch_reply_command (void *ctx, int argc, char *argv[])
 {
     notmuch_config_t *config;
     notmuch_database_t *notmuch;
     notmuch_query_t *query;
-    char *opt, *query_string;
-    int i, ret = 0;
-    int (*reply_format_func)(void *ctx, notmuch_config_t *config, notmuch_query_t *query, notmuch_show_params_t *params);
+    char *query_string;
+    int opt_index, ret = 0;
+    int (*reply_format_func)(void *ctx, notmuch_config_t *config, notmuch_query_t *query, notmuch_show_params_t *params, notmuch_bool_t reply_all);
     notmuch_show_params_t params = { .part = -1 };
+    int format = FORMAT_DEFAULT;
+    int reply_all = TRUE;
+    notmuch_bool_t decrypt = FALSE;
+
+    notmuch_opt_desc_t options[] = {
+       { NOTMUCH_OPT_KEYWORD, &format, "format", 'f',
+         (notmuch_keyword_t []){ { "default", FORMAT_DEFAULT },
+                                 { "headers-only", FORMAT_HEADERS_ONLY },
+                                 { 0, 0 } } },
+       { NOTMUCH_OPT_KEYWORD, &reply_all, "reply-to", 'r',
+         (notmuch_keyword_t []){ { "all", TRUE },
+                                 { "sender", FALSE },
+                                 { 0, 0 } } },
+       { NOTMUCH_OPT_BOOLEAN, &decrypt, "decrypt", 'd', 0 },
+       { 0, 0, 0, 0, 0 }
+    };
 
-    reply_format_func = notmuch_reply_format_default;
-
-    argc--; argv++; /* skip subcommand argument */
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0) {
+       /* diagnostics already printed */
+       return 1;
+    }
 
-    for (i = 0; i < argc && argv[i][0] == '-'; i++) {
-       if (strcmp (argv[i], "--") == 0) {
-           i++;
-           break;
-       }
-        if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
-           opt = argv[i] + sizeof ("--format=") - 1;
-           if (strcmp (opt, "default") == 0) {
-               reply_format_func = notmuch_reply_format_default;
-           } else if (strcmp (opt, "headers-only") == 0) {
-               reply_format_func = notmuch_reply_format_headers_only;
-           } else {
-               fprintf (stderr, "Invalid value for --format: %s\n", opt);
-               return 1;
-           }
-       } else if ((STRNCMP_LITERAL (argv[i], "--decrypt") == 0)) {
-           if (params.cryptoctx == NULL) {
-               GMimeSession* session = g_object_new(g_mime_session_get_type(), NULL);
-               if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg"))) {
-                   fprintf (stderr, "Failed to construct gpg context.\n");
-               } else {
-                   params.decrypt = TRUE;
-                   g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
-               }
-               g_object_unref (session);
-               session = NULL;
-           }
+    if (format == FORMAT_HEADERS_ONLY)
+       reply_format_func = notmuch_reply_format_headers_only;
+    else
+       reply_format_func = notmuch_reply_format_default;
+
+    if (decrypt) {
+#ifdef GMIME_ATLEAST_26
+       /* TODO: GMimePasswordRequestFunc */
+       params.cryptoctx = g_mime_gpg_context_new (NULL, "gpg");
+#else
+       GMimeSession* session = g_object_new (g_mime_session_get_type(), NULL);
+       params.cryptoctx = g_mime_gpg_context_new (session, "gpg");
+#endif
+       if (params.cryptoctx) {
+           g_mime_gpg_context_set_always_trust ((GMimeGpgContext*) params.cryptoctx, FALSE);
+           params.decrypt = TRUE;
        } else {
-           fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
-           return 1;
+           fprintf (stderr, "Failed to construct gpg context.\n");
        }
+#ifndef GMIME_ATLEAST_26
+       g_object_unref (session);
+#endif
     }
 
-    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);
+    query_string = query_string_from_args (ctx, argc-opt_index, argv+opt_index);
     if (query_string == NULL) {
        fprintf (stderr, "Out of memory\n");
        return 1;
@@ -690,7 +732,7 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
        return 1;
     }
 
-    if (reply_format_func (ctx, config, query, &params) != 0)
+    if (reply_format_func (ctx, config, query, &params, reply_all) != 0)
        return 1;
 
     notmuch_query_destroy (query);
index 4baab5612efefafdacab08fe8a74eb72e8352cda..d504051c618ae9f2444365615c24d620b919a659 100644 (file)
@@ -423,6 +423,9 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
     int limit = -1; /* unlimited */
+    const char **search_exclude_tags;
+    size_t search_exclude_tags_length;
+    unsigned int i;
 
     enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT }
        format_sel = NOTMUCH_FORMAT_TEXT;
@@ -490,6 +493,11 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     notmuch_query_set_sort (query, sort);
 
+    search_exclude_tags = notmuch_config_get_search_exclude_tags
+       (config, &search_exclude_tags_length);
+    for (i = 0; i < search_exclude_tags_length; i++)
+       notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+
     switch (output) {
     default:
     case OUTPUT_SUMMARY:
index c3ea9371c13cbc4058922fbdc920a5755e4084f0..94d0aa7bace6e96a8eb417ace7a71a8a8ed800d5 100644 (file)
@@ -87,6 +87,38 @@ welcome_message_post_setup (void)
 "have sufficient storage space available now.\n\n");
 }
 
+static void
+print_tag_list (const char **tags, size_t tags_len)
+{
+    unsigned int i;
+    for (i = 0; i < tags_len; i++) {
+       if (i != 0)
+           printf (" ");
+       printf ("%s", tags[i]);
+    }
+}
+
+static GPtrArray *
+parse_tag_list (void *ctx, char *response)
+{
+    GPtrArray *tags = g_ptr_array_new ();
+    char *tag = response;
+    char *space;
+
+    while (tag && *tag) {
+       space = strchr (tag, ' ');
+       if (space)
+           g_ptr_array_add (tags, talloc_strndup (ctx, tag, space - tag));
+       else
+           g_ptr_array_add (tags, talloc_strdup (ctx, tag));
+       tag = space;
+       while (tag && *tag == ' ')
+           tag++;
+    }
+
+    return tags;
+}
+
 int
 notmuch_setup_command (unused (void *ctx),
                       unused (int argc), unused (char *argv[]))
@@ -101,6 +133,8 @@ notmuch_setup_command (unused (void *ctx),
     int is_new;
     const char **new_tags;
     size_t new_tags_len;
+    const char **search_exclude_tags;
+    size_t search_exclude_tags_len;
 
 #define prompt(format, ...)                                    \
     do {                                                       \
@@ -164,37 +198,36 @@ notmuch_setup_command (unused (void *ctx),
     new_tags = notmuch_config_get_new_tags (config, &new_tags_len);
 
     printf ("Tags to apply to all new messages (separated by spaces) [");
+    print_tag_list (new_tags, new_tags_len);
+    prompt ("]: ");
 
-    for (i = 0; i < new_tags_len; i++) {
-       if (i != 0)
-           printf (" ");
-       printf ("%s", new_tags[i]);
+    if (strlen (response)) {
+       GPtrArray *tags = parse_tag_list (ctx, response);
+
+       notmuch_config_set_new_tags (config, (const char **) tags->pdata,
+                                    tags->len);
+
+       g_ptr_array_free (tags, TRUE);
     }
 
+
+    search_exclude_tags = notmuch_config_get_search_exclude_tags (config, &search_exclude_tags_len);
+
+    printf ("Tags to exclude when searching messages (separated by spaces) [");
+    print_tag_list (search_exclude_tags, search_exclude_tags_len);
     prompt ("]: ");
 
     if (strlen (response)) {
-       GPtrArray *tags = g_ptr_array_new ();
-       char *tag = response;
-       char *space;
-
-       while (tag && *tag) {
-           space = strchr (tag, ' ');
-           if (space)
-               g_ptr_array_add (tags, talloc_strndup (ctx, tag, space - tag));
-           else
-               g_ptr_array_add (tags, talloc_strdup (ctx, tag));
-           tag = space;
-           while (tag && *tag == ' ')
-               tag++;
-       }
+       GPtrArray *tags = parse_tag_list (ctx, response);
 
-       notmuch_config_set_new_tags (config, (const char **) tags->pdata,
-                                    tags->len);
+       notmuch_config_set_search_exclude_tags (config,
+                                               (const char **) tags->pdata,
+                                               tags->len);
 
        g_ptr_array_free (tags, TRUE);
     }
 
+
     if (! notmuch_config_save (config)) {
        if (is_new)
          welcome_message_post_setup ();
index 19fb49f29b89172b63eed772ab79297e986904a1..dec799c7a0ed91801f951868b38b6b3544317ae7 100644 (file)
@@ -42,7 +42,7 @@ static void
 format_part_end_text (GMimeObject *part);
 
 static const notmuch_show_format_t format_text = {
-    "",
+    "", NULL,
        "\fmessage{ ", format_message_text,
            "\fheader{\n", format_headers_text, format_headers_message_part_text, "\fheader}\n",
            "\fbody{\n",
@@ -76,7 +76,11 @@ static void
 format_part_encstatus_json (int status);
 
 static void
+#ifdef GMIME_ATLEAST_26
+format_part_sigstatus_json (GMimeSignatureList* siglist);
+#else
 format_part_sigstatus_json (const GMimeSignatureValidity* validity);
+#endif
 
 static void
 format_part_content_json (GMimeObject *part);
@@ -85,7 +89,7 @@ static void
 format_part_end_json (GMimeObject *part);
 
 static const notmuch_show_format_t format_json = {
-    "[",
+    "[", NULL,
        "{", format_message_json,
            "\"headers\": {", format_headers_json, format_headers_message_part_json, "}",
            ", \"body\": [",
@@ -106,7 +110,7 @@ format_message_mbox (const void *ctx,
                     unused (int indent));
 
 static const notmuch_show_format_t format_mbox = {
-    "",
+    "", NULL,
         "", format_message_mbox,
             "", NULL, NULL, "",
             "",
@@ -125,7 +129,7 @@ static void
 format_part_content_raw (GMimeObject *part);
 
 static const notmuch_show_format_t format_raw = {
-    "",
+    "", NULL,
        "", NULL,
            "", NULL, format_headers_message_part_text, "\n",
             "",
@@ -360,6 +364,7 @@ format_headers_message_part_text (GMimeMessage *message)
     InternetAddressList *recipients;
     const char *recipients_string;
 
+    printf ("Subject: %s\n", g_mime_message_get_subject (message));
     printf ("From: %s\n", g_mime_message_get_sender (message));
     recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
     recipients_string = internet_address_list_to_string (recipients, 0);
@@ -371,7 +376,6 @@ format_headers_message_part_text (GMimeMessage *message)
     if (recipients_string)
        printf ("Cc: %s\n",
                recipients_string);
-    printf ("Subject: %s\n", g_mime_message_get_subject (message));
     printf ("Date: %s\n", g_mime_message_get_date_as_string (message));
 }
 
@@ -486,6 +490,21 @@ show_text_part_content (GMimeObject *part, GMimeStream *stream_out)
        g_object_unref(stream_filter);
 }
 
+#ifdef GMIME_ATLEAST_26
+static const char*
+signature_status_to_string (GMimeSignatureStatus x)
+{
+    switch (x) {
+    case GMIME_SIGNATURE_STATUS_GOOD:
+       return "good";
+    case GMIME_SIGNATURE_STATUS_BAD:
+       return "bad";
+    case GMIME_SIGNATURE_STATUS_ERROR:
+       return "error";
+    }
+    return "unknown";
+}
+#else
 static const char*
 signer_status_to_string (GMimeSignerStatus x)
 {
@@ -501,6 +520,7 @@ signer_status_to_string (GMimeSignerStatus x)
     }
     return "unknown";
 }
+#endif
 
 static void
 format_part_start_text (GMimeObject *part, int *part_count)
@@ -592,6 +612,73 @@ format_part_encstatus_json (int status)
     printf ("}]");
 }
 
+#ifdef GMIME_ATLEAST_26
+static void
+format_part_sigstatus_json (GMimeSignatureList *siglist)
+{
+    printf (", \"sigstatus\": [");
+
+    if (!siglist) {
+       printf ("]");
+       return;
+    }
+
+    void *ctx_quote = talloc_new (NULL);
+    int i;
+    for (i = 0; i < g_mime_signature_list_length (siglist); i++) {
+       GMimeSignature *signature = g_mime_signature_list_get_signature (siglist, i);
+
+       if (i > 0)
+           printf (", ");
+
+       printf ("{");
+
+       /* status */
+       GMimeSignatureStatus status = g_mime_signature_get_status (signature);
+       printf ("\"status\": %s",
+               json_quote_str (ctx_quote,
+                               signature_status_to_string (status)));
+
+       GMimeCertificate *certificate = g_mime_signature_get_certificate (signature);
+       if (status == GMIME_SIGNATURE_STATUS_GOOD) {
+           if (certificate)
+               printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, g_mime_certificate_get_fingerprint (certificate)));
+           /* these dates are seconds since the epoch; should we
+            * provide a more human-readable format string? */
+           time_t created = g_mime_signature_get_created (signature);
+           if (created != -1)
+               printf (", \"created\": %d", (int) created);
+           time_t expires = g_mime_signature_get_expires (signature);
+           if (expires > 0)
+               printf (", \"expires\": %d", (int) expires);
+           /* output user id only if validity is FULL or ULTIMATE. */
+           /* note that gmime is using the term "trust" here, which
+            * is WRONG.  It's actually user id "validity". */
+           if (certificate) {
+               const char *name = g_mime_certificate_get_name (certificate);
+               GMimeCertificateTrust trust = g_mime_certificate_get_trust (certificate);
+               if (name && (trust == GMIME_CERTIFICATE_TRUST_FULLY || trust == GMIME_CERTIFICATE_TRUST_ULTIMATE))
+                   printf (", \"userid\": %s", json_quote_str (ctx_quote, name));
+           }
+       } else if (certificate) {
+           const char *key_id = g_mime_certificate_get_key_id (certificate);
+           if (key_id)
+               printf (", \"keyid\": %s", json_quote_str (ctx_quote, key_id));
+       }
+
+       GMimeSignatureError errors = g_mime_signature_get_errors (signature);
+       if (errors != GMIME_SIGNATURE_ERROR_NONE) {
+           printf (", \"errors\": %d", errors);
+       }
+
+       printf ("}");
+     }
+
+    printf ("]");
+
+    talloc_free (ctx_quote);
+}
+#else
 static void
 format_part_sigstatus_json (const GMimeSignatureValidity* validity)
 {
@@ -641,7 +728,7 @@ format_part_sigstatus_json (const GMimeSignatureValidity* validity)
                printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
        }
        if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
-           printf (", \"errors\": %x", signer->errors);
+           printf (", \"errors\": %d", signer->errors);
        }
 
        printf ("}");
@@ -652,6 +739,7 @@ format_part_sigstatus_json (const GMimeSignatureValidity* validity)
 
     talloc_free (ctx_quote);
 }
+#endif
 
 static void
 format_part_content_json (GMimeObject *part)
@@ -675,13 +763,31 @@ format_part_content_json (GMimeObject *part)
            printf (", \"filename\": %s", json_quote_str (ctx, filename));
     }
 
-    if (g_mime_content_type_is_type (content_type, "text", "*") &&
-       !g_mime_content_type_is_type (content_type, "text", "html"))
+    if (g_mime_content_type_is_type (content_type, "text", "*"))
     {
-       show_text_part_content (part, stream_memory);
-       part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
+       /* For non-HTML text parts, we include the content in the
+        * JSON. Since JSON must be Unicode, we handle charset
+        * decoding here and do not report a charset to the caller.
+        * For text/html parts, we do not include the content. If a
+        * caller is interested in text/html parts, it should retrieve
+        * them separately and they will not be decoded. Since this
+        * makes charset decoding the responsibility on the caller, we
+        * report the charset for text/html parts.
+        */
+       if (g_mime_content_type_is_type (content_type, "text", "html"))
+       {
+           const char *content_charset = g_mime_object_get_content_type_parameter (GMIME_OBJECT (part), "charset");
+
+           if (content_charset != NULL)
+               printf (", \"content-charset\": %s", json_quote_str (ctx, content_charset));
+       }
+       else
+       {
+           show_text_part_content (part, stream_memory);
+           part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
 
-       printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
+           printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
+       }
     }
     else if (g_mime_content_type_is_type (content_type, "multipart", "*"))
     {
@@ -744,6 +850,19 @@ show_message (void *ctx,
              int indent,
              notmuch_show_params_t *params)
 {
+    if (format->part) {
+       void *local = talloc_new (ctx);
+       mime_node_t *root, *part;
+
+       if (mime_node_open (local, message, params->cryptoctx, params->decrypt,
+                           &root) == NOTMUCH_STATUS_SUCCESS &&
+           (part = mime_node_seek_dfs (root, (params->part < 0 ?
+                                              0 : params->part))))
+           format->part (local, part, indent, params);
+       talloc_free (local);
+       return;
+    }
+
     if (params->part <= 0) {
        fputs (format->message_start, stdout);
        if (format->message)
@@ -758,8 +877,7 @@ show_message (void *ctx,
     }
 
     if (format->part_content)
-       show_message_body (notmuch_message_get_filename (message),
-                          format, params);
+       show_message_body (message, format, params);
 
     if (params->part <= 0) {
        fputs (format->body_end, stdout);
@@ -866,7 +984,17 @@ do_show_single (void *ctx,
 
        while (!feof (file)) {
            size = fread (buf, 1, sizeof (buf), file);
-           (void) fwrite (buf, size, 1, stdout);
+           if (ferror (file)) {
+               fprintf (stderr, "Error: Read failed from %s\n", filename);
+               fclose (file);
+               return 1;
+           }
+
+           if (fwrite (buf, size, 1, stdout) != 1) {
+               fprintf (stderr, "Error: Write failed\n");
+               fclose (file);
+               return 1;
+           }
        }
 
        fclose (file);
@@ -973,13 +1101,20 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        } else if ((STRNCMP_LITERAL (argv[i], "--verify") == 0) ||
                   (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)) {
            if (params.cryptoctx == NULL) {
+#ifdef GMIME_ATLEAST_26
+               /* TODO: GMimePasswordRequestFunc */
+               if (NULL == (params.cryptoctx = g_mime_gpg_context_new(NULL, "gpg")))
+#else
                GMimeSession* session = g_object_new(g_mime_session_get_type(), NULL);
                if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg")))
+#endif
                    fprintf (stderr, "Failed to construct gpg context.\n");
                else
                    g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
+#ifndef GMIME_ATLEAST_26
                g_object_unref (session);
                session = NULL;
+#endif
            }
            if (STRNCMP_LITERAL (argv[i], "--decrypt") == 0)
                params.decrypt = 1;
index 292c5da3f824a222caceed290109cabed40e959c..44fd61f6811db84d8e437f37247404140764556a 100644 (file)
@@ -26,7 +26,12 @@ static void
 handle_sigint (unused (int sig))
 {
     static char msg[] = "Stopping...         \n";
-    (void) write(2, msg, sizeof(msg)-1);
+
+    /* This write is "opportunistic", so it's okay to ignore the
+     * result.  It is not required for correctness, and if it does
+     * fail or produce a short write, we want to get out of the signal
+     * handler as quickly as possible, not retry it. */
+    IGNORE_RESULT (write (2, msg, sizeof(msg)-1));
     interrupted = 1;
 }
 
diff --git a/notmuch.1 b/notmuch.1
deleted file mode 100644 (file)
index 7ab2947..0000000
--- a/notmuch.1
+++ /dev/null
@@ -1,776 +0,0 @@
-.\" notmuch - Not much of an email program, (just index, search and tagging)
-.\"
-.\" Copyright © 2009 Carl Worth
-.\"
-.\" 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 this program.  If not, see http://www.gnu.org/licenses/ .
-.\"
-.\" Author: Carl Worth <cworth@cworth.org>
-.TH NOTMUCH 1 2012-02-03 "Notmuch 0.11.1"
-.SH NAME
-notmuch \- thread-based email index, search, and tagging
-.SH SYNOPSIS
-.B notmuch
-.IR command " [" args " ...]"
-.SH DESCRIPTION
-Notmuch is a command-line based program for indexing, searching,
-reading, and tagging large collections of email messages.
-
-The quickest way to get started with Notmuch is to simply invoke the
-.B notmuch
-command with no arguments, which will interactively guide you through
-the process of indexing your mail.
-.SH NOTE
-While the command-line program
-.B notmuch
-provides powerful functionality, it does not provide the most
-convenient interface for that functionality. More sophisticated
-interfaces are expected to be built on top of either the command-line
-interface, or more likely, on top of the notmuch library
-interface. See http://notmuchmail.org for more about alternate
-interfaces to notmuch.
-.SH COMMANDS
-The
-.BR setup
-command is used to configure Notmuch for first use, (or to reconfigure
-it later).
-.RS 4
-.TP 4
-.B setup
-
-Interactively sets up notmuch for first use.
-
-The setup command will prompt for your full name, your primary email
-address, any alternate email addresses you use, and the directory
-containing your email archives. Your answers will be written to a
-configuration file in ${NOTMUCH_CONFIG} (if set) or
-${HOME}/.notmuch-config . This configuration file will be created with
-descriptive comments, making it easy to edit by hand later to change the
-configuration. Or you can run
-.B "notmuch setup"
-again to change the configuration.
-
-The mail directory you specify can contain any number of
-sub-directories and should primarily contain only files with individual
-email messages (eg. maildir or mh archives are perfect). If there are
-other, non-email files (such as indexes maintained by other email
-programs) then notmuch will do its best to detect those and ignore
-them.
-
-Mail storage that uses mbox format, (where one mbox file contains many
-messages), will not work with notmuch. If that's how your mail is
-currently stored, it is recommended you first convert it to maildir
-format with a utility such as mb2md before running
-.B "notmuch setup" .
-
-Invoking
-.B notmuch
-with no command argument will run
-.B setup
-if the setup command has not previously been completed.
-.RE
-
-The
-.B new
-command is used to incorporate new mail into the notmuch database.
-.RS 4
-.TP 4
-.BR new " [options...]"
-
-Find and import any new messages to the database.
-
-The
-.B new
-command scans all sub-directories of the database, performing
-full-text indexing on new messages that are found. Each new message
-will automatically be tagged with both the
-.BR inbox " and " unread
-tags.
-
-You should run
-.B "notmuch new"
-once after first running
-.B "notmuch setup"
-to create the initial database. The first run may take a long time if
-you have a significant amount of mail (several hundred thousand
-messages or more). Subsequently, you should run
-.B "notmuch new"
-whenever new mail is delivered and you wish to incorporate it into the
-database. These subsequent runs will be much quicker than the initial
-run.
-
-Invoking
-.B notmuch
-with no command argument will run
-.B new
-if
-.B "notmuch setup"
-has previously been completed, but
-.B "notmuch new"
-has not previously been run.
-
-The
-.B new
-command supports hooks. See the
-.B "HOOKS"
-section below for more details on hooks.
-
-Supported options for
-.B new
-include
-.RS 4
-.TP 4
-.BR \-\-no\-hooks
-
-Prevents hooks from being run.
-.RE
-.RE
-
-Several of the notmuch commands accept search terms with a common
-syntax. See the
-.B "SEARCH SYNTAX"
-section below for more details on the supported syntax.
-
-The
-.BR search ", " show " and " count
-commands are used to query the email database.
-.RS 4
-.TP 4
-.BR search " [options...] <search-term>..."
-
-Search for messages matching the given search terms, and display as
-results the threads containing the matched messages.
-
-The output consists of one line per thread, giving a thread ID, the
-date of the newest (or oldest, depending on the sort option) matched
-message in the thread, the number of matched messages and total
-messages in the thread, the names of all participants in the thread,
-and the subject of the newest (or oldest) message.
-
-Supported options for
-.B search
-include
-.RS 4
-.TP 4
-.BR \-\-format= ( json | text )
-
-Presents the results in either JSON or plain-text (default).
-.RE
-
-.RS 4
-.TP 4
-.B \-\-output=(summary|threads|messages|files|tags)
-
-.RS 4
-.TP 4
-.B summary
-
-Output a summary of each thread with any message matching the search
-terms. The summary includes the thread ID, date, the number of
-messages in the thread (both the number matched and the total number),
-the authors of the thread and the subject.
-.RE
-.RS 4
-.TP 4
-.B threads
-
-Output the thread IDs of all threads with any message matching the
-search terms, either one per line (\-\-format=text) or as a JSON array
-(\-\-format=json).
-.RE
-.RS 4
-.TP 4
-.B messages
-
-Output the message IDs of all messages matching the search terms,
-either one per line (\-\-format=text) or as a JSON array
-(\-\-format=json).
-.RE
-.RS 4
-.TP 4
-.B files
-
-Output the filenames of all messages matching the search terms, either
-one per line (\-\-format=text) or as a JSON array (\-\-format=json).
-.RE
-.RS 4
-.TP 4
-.B tags
-
-Output all tags that appear on any message matching the search terms,
-either one per line (\-\-format=text) or as a JSON array
-(\-\-format=json).
-.RE
-.RE
-
-.RS 4
-.TP 4
-.BR \-\-sort= ( newest\-first | oldest\-first )
-
-This option can be used to present results in either chronological order
-.RB ( oldest\-first )
-or reverse chronological order
-.RB ( newest\-first ).
-
-Note: The thread order will be distinct between these two options
-(beyond being simply reversed). When sorting by
-.B oldest\-first
-the threads will be sorted by the oldest message in each thread, but
-when sorting by
-.B newest\-first
-the threads will be sorted by the newest message in each thread.
-
-By default, results will be displayed in reverse chronological order,
-(that is, the newest results will be displayed first).
-.RE
-
-.RS 4
-.TP 4
-.BR \-\-offset=[\-]N
-
-Skip displaying the first N results. With the leading '\-', start at the Nth
-result from the end.
-.RE
-
-.RS 4
-.TP 4
-.BR \-\-limit=N
-
-Limit the number of displayed results to N.
-.RE
-
-.RS 4
-See the
-.B "SEARCH SYNTAX"
-section below for details of the supported syntax for <search-terms>.
-.RE
-.TP
-.BR show " [options...] <search-term>..."
-
-Shows all messages matching the search terms.
-
-The messages will be grouped and sorted based on the threading (all
-replies to a particular message will appear immediately after that
-message in date order). The output is not indented by default, but
-depth tags are printed so that proper indentation can be performed by
-a post-processor (such as the emacs interface to notmuch).
-
-Supported options for
-.B show
-include
-.RS 4
-.TP 4
-.B \-\-entire\-thread
-
-By default only those messages that match the search terms will be
-displayed. With this option, all messages in the same thread as any
-matched message will be displayed.
-.RE
-
-.RS 4
-.TP 4
-.B \-\-format=(text|json|mbox|raw)
-
-.RS 4
-.TP 4
-.BR text " (default for messages)"
-
-The default plain-text format has all 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
-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. For a multipart MIME message, these parts will
-be nested.
-.RE
-.RS 4
-.TP 4
-.B json
-
-The output is formatted with Javascript Object Notation (JSON). This
-format is more robust than the text format for automated
-processing. The nested structure of multipart MIME messages is
-reflected in nested JSON output. JSON output always includes all
-messages in a matching thread; in effect
-.B \-\-format=json
-implies
-.B \-\-entire\-thread
-
-.RE
-.RS 4
-.TP 4
-.B mbox
-
-All matching messages are output in the traditional, Unix mbox format
-with each message being prefixed by a line beginning with "From " and
-a blank line separating each message. Lines in the message content
-beginning with "From " (preceded by zero or more '>' characters) have
-an additional '>' character added. This reversible escaping
-is termed "mboxrd" format and described in detail here:
-
-.nf
-.nh
-http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html
-.hy
-.fi
-.
-.RE
-.RS 4
-.TP 4
-.BR raw " (default for a single part, see \-\-part)"
-
-For a message, the original, raw content of the email message is
-output. Consumers of this format should expect to implement MIME
-decoding and similar functions.
-
-For a single part (\-\-part) the raw part content is output after
-performing any necessary MIME decoding.
-
-The raw format must only be used with search terms matching single
-message.
-.RE
-.RE
-
-.RS 4
-.TP 4
-.B \-\-part=N
-
-Output the single decoded MIME part N of a single message.  The search
-terms must match only a single message.  Message parts are numbered in
-a depth-first walk of the message MIME structure, and are identified
-in the 'json' or 'text' output formats.
-.RE
-
-.RS 4
-.TP 4
-.B \-\-verify
-
-Compute and report the validity of any MIME cryptographic signatures
-found in the selected content (ie. "multipart/signed" parts). Status
-of the signature will be reported (currently only supported with
---format=json), and the multipart/signed part will be replaced by the
-signed data.
-.RE
-
-.RS 4
-.TP 4
-.B \-\-decrypt
-
-Decrypt any MIME encrypted parts found in the selected content
-(ie. "multipart/encrypted" parts). Status of the decryption will be
-reported (currently only supported with --format=json) and the
-multipart/encrypted part will be replaced by the decrypted
-content.
-.RE
-
-A common use of
-.B notmuch show
-is to display a single thread of email messages. For this, use a
-search term of "thread:<thread-id>" as can be seen in the first
-column of output from the
-.B notmuch search
-command.
-
-See the
-.B "SEARCH SYNTAX"
-section below for details of the supported syntax for <search-terms>.
-.RE
-.RS 4
-.TP 4
-.BR count " [options...] <search-term>..."
-
-Count messages matching the search terms.
-
-The number of matching messages (or threads) is output to stdout.
-
-With no search terms, a count of all messages (or threads) in the database will
-be displayed.
-
-Supported options for
-.B count
-include
-.RS 4
-.TP 4
-.B \-\-output=(messages|threads)
-
-.RS 4
-.TP 4
-.B messages
-
-Output the number of matching messages. This is the default.
-.RE
-.RS 4
-.TP 4
-.B threads
-
-Output the number of matching threads.
-.RE
-.RE
-.RE
-.RE
-
-The
-.B reply
-command is useful for preparing a template for an email reply.
-.RS 4
-.TP 4
-.BR reply " [options...] <search-term>..."
-
-Constructs a reply template for a set of messages.
-
-To make replying to email easier,
-.B notmuch reply
-takes an existing set of messages and constructs a suitable mail
-template. The Reply-to header (if any, otherwise From:) is used for
-the To: address. Vales from the To: and Cc: headers are copied, but
-not including any of the current user's email addresses (as configured
-in primary_mail or other_email in the .notmuch\-config file) in the
-recipient list
-
-It also builds a suitable new subject, including Re: at the front (if
-not already present), and adding the message IDs of the messages being
-replied to to the References list and setting the In\-Reply\-To: field
-correctly.
-
-Finally, the original contents of the emails are quoted by prefixing
-each line with '> ' and included in the body.
-
-The resulting message template is output to stdout.
-
-Supported options for
-.B reply
-include
-.RS
-.TP 4
-.BR \-\-format= ( default | headers\-only )
-.RS
-.TP 4
-.BR default
-Includes subject and quoted message body.
-.TP
-.BR headers\-only
-Only produces In\-Reply\-To, References, To, Cc, and Bcc headers.
-.RE
-
-See the
-.B "SEARCH SYNTAX"
-section below for details of the supported syntax for <search-terms>.
-
-Note: It is most common to use
-.B "notmuch reply"
-with a search string matching a single message, (such as
-id:<message-id>), but it can be useful to reply to several messages at
-once. For example, when a series of patches are sent in a single
-thread, replying to the entire thread allows for the reply to comment
-on issue found in multiple patches.
-.RE
-.RE
-
-The
-.B tag
-command is the only command available for manipulating database
-contents.
-
-.RS 4
-.TP 4
-.BR tag " +<tag>|\-<tag> [...] [\-\-] <search-term>..."
-
-Add/remove tags for all messages matching the search terms.
-
-Tags prefixed by '+' are added while those prefixed by '\-' are
-removed. For each message, tag removal is performed before tag
-addition.
-
-The beginning of <search-terms> is recognized by the first
-argument that begins with neither '+' nor '\-'. Support for
-an initial search term beginning with '+' or '\-' is provided
-by allowing the user to specify a "\-\-" argument to separate
-the tags from the search terms.
-
-See the
-.B "SEARCH SYNTAX"
-section below for details of the supported syntax for <search-terms>.
-.RE
-
-The
-.BR dump " and " restore
-commands can be used to create a textual dump of email tags for backup
-purposes, and to restore from that dump.
-
-.RS 4
-.TP 4
-.BR dump " [<filename>] [--] [<search-terms>]"
-
-Creates a plain-text dump of the tags of each message.
-
-Output is to the given filename, if any, or to stdout.  Note that
-using the filename argument is deprecated.
-
-These tags are the only data in the notmuch database that can't be
-recreated from the messages themselves.  The output of notmuch dump is
-therefore the only critical thing to backup (and much more friendly to
-incremental backup than the native database files.)
-
-With no search terms, a dump of all messages in the database will be
-generated.  A "--" argument instructs notmuch that the
-remaining arguments are search terms.
-
-See the
-.B "SEARCH SYNTAX"
-section below for details of the supported syntax for <search-terms>.
-.RE
-
-.TP
-.BR restore " [--accumulate] [<filename>]"
-
-Restores the tags from the given file (see
-.BR "notmuch dump" ")."
-
-The input is read from the given filename, if any, or from stdin.
-
-Note: The dump file format is specifically chosen to be
-compatible with the format of files produced by sup-dump.
-So if you've previously been using sup for mail, then the
-.B "notmuch restore"
-command provides you a way to import all of your tags (or labels as
-sup calls them).
-
-The --accumulate switch causes the union of the existing and new tags to be
-applied, instead of replacing each message's tags as they are read in from the
-dump file.
-.RE
-
-The
-.B part
-command can used to output a single part of a multipart 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
-
-The
-.B config
-command can be used to get or set settings int the notmuch
-configuration file.
-
-.RS 4
-.TP 4
-.BR "config get " <section> . <item>
-
-The value of the specified configuration item is printed to stdout. If
-the item has multiple values, each value is separated by a newline
-character.
-
-Available configuration items include at least
-
-       database.path
-
-       user.name
-
-       user.primary_email
-
-       user.other_email
-
-       new.tags
-.RE
-
-.RS 4
-.TP 4
-.BR "config set " <section> . "<item> [values ...]"
-
-The specified configuration item is set to the given value.  To
-specify a multiple-value item, provide each value as a separate
-command-line argument.
-
-If no values are provided, the specified configuration item will be
-removed from the configuration file.
-.RE
-
-.SH SEARCH SYNTAX
-Several notmuch commands accept a common syntax for search terms.
-
-The search terms can consist of free-form text (and quoted phrases)
-which will match all messages that contain all of the given
-terms/phrases in the body, the subject, or any of the sender or
-recipient headers.
-
-As a special case, a search string consisting of exactly a single
-asterisk ("*") will match all messages.
-
-In addition to free text, the following prefixes can be used to force
-terms to match against specific portions of an email, (where
-<brackets> indicate user-supplied values):
-
-       from:<name-or-address>
-
-       to:<name-or-address>
-
-       subject:<word-or-quoted-phrase>
-
-       attachment:<word>
-
-       tag:<tag> (or is:<tag>)
-
-       id:<message-id>
-
-       thread:<thread-id>
-
-       folder:<directory-path>
-
-The
-.B from:
-prefix is used to match the name or address of the sender of an email
-message.
-
-The
-.B to:
-prefix is used to match the names or addresses of any recipient of an
-email message, (whether To, Cc, or Bcc).
-
-Any term prefixed with
-.B subject:
-will match only text from the subject of an email. Searching for a
-phrase in the subject is supported by including quotation marks around
-the phrase, immediately following
-.BR subject: .
-
-The
-.B attachment:
-prefix can be used to search for specific filenames (or extensions) of
-attachments to email messages.
-
-For
-.BR tag: " and " is:
-valid tag values include
-.BR inbox " and " unread
-by default for new messages added by
-.B notmuch new
-as well as any other tag values added manually with
-.BR "notmuch tag" .
-
-For
-.BR id: ,
-message ID values are the literal contents of the Message\-ID: header
-of email messages, but without the '<', '>' delimiters.
-
-The
-.B thread:
-prefix can be used with the thread ID values that are generated
-internally by notmuch (and do not appear in email messages). These
-thread ID values can be seen in the first column of output from
-.B "notmuch search"
-
-The
-.B folder:
-prefix can be used to search for email message files that are
-contained within particular directories within the mail store. Only
-the directory components below the top-level mail database path are
-available to be searched.
-
-In addition to individual terms, multiple terms can be
-combined with Boolean operators (
-.BR and ", " or ", " not
-, etc.). Each term in the query will be implicitly connected by a
-logical AND if no explicit operator is provided, (except that terms
-with a common prefix will be implicitly combined with OR until we get
-Xapian defect #402 fixed).
-
-Parentheses can also be used to control the combination of the Boolean
-operators, but will have to be protected from interpretation by the
-shell, (such as by putting quotation marks around any parenthesized
-expression).
-
-Finally, results can be restricted to only messages within a
-particular time range, (based on the Date: header) with a syntax of:
-
-       <initial-timestamp>..<final-timestamp>
-
-Each timestamp is a number representing the number of seconds since
-1970\-01\-01 00:00:00 UTC. This is not the most convenient means of
-expressing date ranges, but until notmuch is fixed to accept a more
-convenient form, one can use the date program to construct
-timestamps. For example, with the bash shell the following syntax would
-specify a date range to return messages from 2009\-10\-01 until the
-current time:
-
-       $(date +%s \-d 2009\-10\-01)..$(date +%s)
-.SH HOOKS
-Hooks are scripts (or arbitrary executables or symlinks to such) that notmuch
-invokes before and after certain actions. These scripts reside in
-the .notmuch/hooks directory within the database directory and must have
-executable permissions.
-
-The currently available hooks are described below.
-.RS 4
-.TP 4
-.B pre\-new
-This hook is invoked by the
-.B new
-command before scanning or importing new messages into the database. If this
-hook exits with a non-zero status, notmuch will abort further processing of the
-.B new
-command.
-
-Typically this hook is used for fetching or delivering new mail to be imported
-into the database.
-.RE
-.RS 4
-.TP 4
-.B post\-new
-This hook is invoked by the
-.B new
-command after new messages have been imported into the database and initial tags
-have been applied. The hook will not be run if there have been any errors during
-the scan or import.
-
-Typically this hook is used to perform additional query\-based tagging on the
-imported messages.
-.RE
-.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
-in the Notmuch distribution).
-
-The notmuch website:
-.B http://notmuchmail.org
-.SH CONTACT
-Feel free to send questions, comments, or kudos to the notmuch mailing
-list <notmuch@notmuchmail.org> . Subscription is not required before
-posting, but is available from the notmuchmail.org website.
-
-Real-time interaction with the Notmuch community is available via IRC
-(server: irc.freenode.net, channel: #notmuch).
index c0ce026a14ff904d29fbf53ca53b40975855bf52..477a09cf05dd1bd803e8d68bfadf0e3c82660a65 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -29,7 +29,6 @@ typedef struct command {
     command_function_t function;
     const char *arguments;
     const char *summary;
-    const char *documentation;
 } command_t;
 
 #define MAX_ALIAS_SUBSTITUTIONS 3
@@ -47,447 +46,40 @@ alias_t aliases[] = {
 static int
 notmuch_help_command (void *ctx, int argc, char *argv[]);
 
-static const char search_terms_help[] =
-    "\tSeveral notmuch commands accept a common syntax for search\n"
-    "\tterms.\n"
-    "\n"
-    "\tThe search terms can consist of free-form text (and quoted\n"
-    "\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"
-    "\tAs a special case, a search string consisting of exactly a\n"
-    "\tsingle asterisk (\"*\") will match all messages.\n"
-    "\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\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"
-    "\t\tfolder:<directory-path>\n"
-    "\n"
-    "\tThe from: prefix is used to match the name or address of\n"
-    "\tthe sender of an email message.\n"
-    "\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"
-    "\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"
-    "\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"
-    "\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"
-    "\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"
-    "\tThe folder: prefix can be used to search for email message\n"
-    "\tfiles that are contained within particular directories within\n"
-    "\tthe mail store. Only the directory components below the top-level\n"
-    "\tmail database path are available to be searched.\n"
-    "\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"
-    "\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"
-    "\tFinally, results can be restricted to only messages within a\n"
-    "\tparticular time range, (based on the Date: header) with:\n"
-    "\n"
-    "\t\t<intial-timestamp>..<final-timestamp>\n"
-    "\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$(date +%%s -d 2009-10-01)..$(date +%%s)\n\n";
-
-static const char hooks_help[] =
-    "\tHooks are scripts (or arbitrary executables or symlinks to such) that\n"
-    "\tnotmuch invokes before and after certain actions. These scripts reside\n"
-    "\tin the .notmuch/hooks directory within the database directory and must\n"
-    "\thave executable permissions.\n"
-    "\n"
-    "\tThe currently available hooks are described below.\n"
-    "\n"
-    "\tpre-new\n"
-    "\t\tThis hook is invoked by the new command before scanning or\n"
-    "\t\timporting new messages into the database. If this hook exits\n"
-    "\t\twith a non-zero status, notmuch will abort further processing\n"
-    "\t\tof the new command.\n"
-    "\n"
-    "\t\tTypically this hook is used for fetching or delivering new\n"
-    "\t\tmail to be imported into the database.\n"
-    "\n"
-    "\tpost-new\n"
-    "\t\tThis hook is invoked by the new command after new messages\n"
-    "\t\thave been imported into the database and initial tags have\n"
-    "\t\tbeen applied. The hook will not be run if there have been any\n"
-    "\t\terrors during the scan or import.\n"
-    "\n"
-    "\t\tTypically this hook is used to perform additional query-based\n"
-    "\t\ttagging on the imported messages.\n\n";
-
 static command_t commands[] = {
     { "setup", notmuch_setup_command,
       NULL,
-      "Interactively setup notmuch for first use.",
-      "\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." },
+      "Interactively setup notmuch for first use." },
     { "new", notmuch_new_command,
       "[options...]",
-      "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"
-      "\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"
-      "\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"
-      "\tThe new command supports hooks. See \"notmuch help hooks\" for\n"
-      "\tmore details on hooks.\n"
-      "\n"
-      "\tSupported options for new include:\n"
-      "\n"
-      "\t--no-hooks\n"
-      "\n"
-      "\t\tPrevent hooks from being run.\n"
-      "\n"
-      "\t--verbose\n"
-      "\n"
-      "\t\tVerbose operation. Shows paths of message files as\n"
-      "\t\tthey are being indexed.\n"
-      "\n"
-      "\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." },
+      "Find and import new messages to the notmuch database." },
     { "search", notmuch_search_command,
       "[options...] <search-terms> [...]",
-      "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"
-      "\tSupported options for search include:\n"
-      "\n"
-      "\t--format=(json|text)\n"
-      "\n"
-      "\t\tPresents the results in either JSON or\n"
-      "\t\tplain-text (default)\n"
-      "\n"
-      "\t--output=(summary|threads|messages|files|tags)\n"
-      "\n"
-      "\t\tsummary (default)\n"
-      "\n"
-      "\t\tOutput a summary of each thread with any message matching the\n"
-      "\t\tsearch terms. The summary includes the thread ID, date, the\n"
-      "\t\tnumber of messages in the thread (both the number matched and\n"
-      "\t\tthe total number), the authors of the thread and the subject.\n"
-      "\n"
-      "\t\tthreads\n"
-      "\n"
-      "\t\tOutput the thread IDs of all threads with any message matching\n"
-      "\t\tthe search terms, either one per line (--format=text) or as a\n"
-      "\t\tJSON array (--format=json).\n"
-      "\n"
-      "\t\tmessages\n"
-      "\n"
-      "\t\tOutput the message IDs of all messages matching the search\n"
-      "\t\tterms, either one per line (--format=text) or as a JSON array\n"
-      "\t\t(--format=json).\n"
-      "\n"
-      "\t\tfiles\n"
-      "\n"
-      "\t\tOutput the filenames of all messages matching the search\n"
-      "\t\tterms, either one per line (--format=text) or as a JSON array\n"
-      "\t\t(--format=json).\n"
-      "\n"
-      "\t\ttags\n"
-      "\n"
-      "\t\tOutput all tags that appear on any message matching the search\n"
-      "\t\tterms, either one per line (--format=text) or as a JSON array\n"
-      "\t\t(--format=json).\n"
-    &n