aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Bremner <bremner@debian.org>2012-06-17 07:38:59 -0300
committerDavid Bremner <bremner@debian.org>2012-06-17 07:38:59 -0300
commit3dc2ff6fc73d3a057ae0a3f350c42b077d7ec273 (patch)
tree19322fd5112777d877a7e31eef3efbc4ac0ef6cd
parent3b1e8a200dd4740b3c5605256e1535a681367347 (diff)
parent2ef24acf03fdd73e39d2c233016e71f194affbcf (diff)
Merge branch 'release' into squeeze-backports
Conflicts: debian/changelog
-rw-r--r--Makefile.local19
-rw-r--r--NEWS652
-rw-r--r--bindings/go/Makefile52
-rw-r--r--bindings/go/cmds/Makefile11
-rw-r--r--bindings/go/pkg/Makefile17
-rw-r--r--bindings/go/src/notmuch-addrlookup/addrlookup.go (renamed from bindings/go/cmds/notmuch-addrlookup.go)90
-rw-r--r--bindings/go/src/notmuch/notmuch.go (renamed from bindings/go/pkg/notmuch.go)140
-rw-r--r--bindings/python/docs/source/database.rst2
-rw-r--r--bindings/python/docs/source/filesystem.rst2
-rw-r--r--bindings/python/notmuch/database.py96
-rw-r--r--bindings/python/notmuch/directory.py4
-rw-r--r--bindings/python/notmuch/filenames.py75
-rw-r--r--bindings/python/notmuch/globals.py4
-rw-r--r--bindings/python/notmuch/message.py37
-rw-r--r--bindings/python/notmuch/messages.py18
-rw-r--r--bindings/python/notmuch/query.py4
-rw-r--r--bindings/python/notmuch/tag.py20
-rw-r--r--bindings/python/notmuch/thread.py4
-rw-r--r--bindings/python/notmuch/threads.py23
-rw-r--r--bindings/python/notmuch/version.py2
-rw-r--r--bindings/python/setup.py61
-rw-r--r--bindings/ruby/database.c25
-rw-r--r--bindings/ruby/defs.h11
-rw-r--r--bindings/ruby/extconf.rb2
-rw-r--r--bindings/ruby/init.c11
-rw-r--r--bindings/ruby/query.c57
-rw-r--r--command-line-arguments.c34
-rwxr-xr-xconfigure77
-rw-r--r--contrib/notmuch-deliver/src/main.c12
-rw-r--r--contrib/notmuch-mutt/.gitignore2
-rw-r--r--contrib/notmuch-mutt/Makefile12
-rw-r--r--contrib/notmuch-mutt/README59
-rwxr-xr-xcontrib/notmuch-mutt/notmuch-mutt238
-rw-r--r--contrib/notmuch-mutt/notmuch-mutt.rc9
-rw-r--r--debian/changelog34
-rw-r--r--debian/control26
-rw-r--r--debian/libnotmuch3.install (renamed from debian/libnotmuch2.install)0
-rw-r--r--debian/libnotmuch3.symbols (renamed from debian/libnotmuch2.symbols)6
-rw-r--r--debian/notmuch-mutt.docs1
-rw-r--r--debian/notmuch-mutt.install2
-rw-r--r--debian/notmuch-mutt.manpages1
-rwxr-xr-xdebian/rules2
-rw-r--r--devel/TODO8
-rw-r--r--devel/schemata38
-rw-r--r--emacs/Makefile.local1
-rw-r--r--emacs/notmuch-hello.el653
-rw-r--r--emacs/notmuch-lib.el95
-rw-r--r--emacs/notmuch-message.el3
-rw-r--r--emacs/notmuch-mua.el200
-rw-r--r--emacs/notmuch-print.el2
-rw-r--r--emacs/notmuch-show.el186
-rw-r--r--emacs/notmuch-tag.el145
-rw-r--r--emacs/notmuch.el180
-rw-r--r--lib/Makefile.local2
-rw-r--r--lib/database.cc115
-rw-r--r--lib/directory.cc41
-rw-r--r--lib/message.cc11
-rw-r--r--lib/notmuch-private.h43
-rw-r--r--lib/notmuch.h116
-rw-r--r--lib/query.cc108
-rw-r--r--lib/thread.cc18
-rw-r--r--man/man1/notmuch-config.124
-rw-r--r--man/man1/notmuch-count.110
-rw-r--r--man/man1/notmuch-dump.12
-rw-r--r--man/man1/notmuch-new.12
-rw-r--r--man/man1/notmuch-reply.122
-rw-r--r--man/man1/notmuch-restore.12
-rw-r--r--man/man1/notmuch-search.115
-rw-r--r--man/man1/notmuch-show.134
-rw-r--r--man/man1/notmuch-tag.12
-rw-r--r--man/man1/notmuch.12
-rw-r--r--man/man5/notmuch-hooks.52
-rw-r--r--man/man7/notmuch-search-terms.72
-rw-r--r--notmuch-client.h55
-rw-r--r--notmuch-config.c71
-rw-r--r--notmuch-count.c33
-rw-r--r--notmuch-dump.c7
-rw-r--r--notmuch-new.c113
-rw-r--r--notmuch-reply.c295
-rw-r--r--notmuch-restore.c155
-rw-r--r--notmuch-search.c47
-rw-r--r--notmuch-setup.c19
-rw-r--r--notmuch-show.c837
-rw-r--r--notmuch-tag.c180
-rw-r--r--show-message.c106
-rwxr-xr-xtest/config60
-rwxr-xr-xtest/crypto44
-rwxr-xr-xtest/emacs217
-rwxr-xr-xtest/emacs-hello69
-rw-r--r--test/emacs.expected-output/notmuch-hello5
-rw-r--r--test/emacs.expected-output/notmuch-hello-empty-custom-queries-section3
-rw-r--r--test/emacs.expected-output/notmuch-hello-empty-custom-tags-section5
-rw-r--r--test/emacs.expected-output/notmuch-hello-long-names18
-rw-r--r--test/emacs.expected-output/notmuch-hello-new-section4
-rw-r--r--test/emacs.expected-output/notmuch-hello-no-saved-searches3
-rw-r--r--test/emacs.expected-output/notmuch-hello-section-counts5
-rw-r--r--test/emacs.expected-output/notmuch-hello-section-hidden-tag4
-rw-r--r--test/emacs.expected-output/notmuch-hello-section-with-empty4
-rw-r--r--test/emacs.expected-output/notmuch-hello-with-empty5
-rwxr-xr-xtest/encoding4
-rwxr-xr-xtest/excludes423
-rwxr-xr-xtest/json6
-rwxr-xr-xtest/maildir-sync3
-rwxr-xr-xtest/multipart143
-rwxr-xr-xtest/notmuch-test3
-rwxr-xr-xtest/python8
-rwxr-xr-xtest/raw7
-rwxr-xr-xtest/search25
-rwxr-xr-xtest/search-folder-coherence2
-rw-r--r--test/symbol-test.cc3
-rwxr-xr-xtest/tagging8
-rw-r--r--test/test-lib.sh6
-rwxr-xr-xtest/thread-naming16
-rw-r--r--version2
-rw-r--r--vim/Makefile19
-rw-r--r--vim/plugin/notmuch.vim5
116 files changed, 4734 insertions, 2351 deletions
diff --git a/Makefile.local b/Makefile.local
index 1131dea8..53b4a0de 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -256,22 +256,12 @@ endif
quiet ?= $($(shell echo $1 | sed -e s'/ .*//'))
%.o: %.cc $(global_deps)
- $(call quiet,CXX $(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@
+ @mkdir -p .deps/$(@D)
+ $(call quiet,CXX $(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@ -MD -MP -MF .deps/$*.d
%.o: %.c $(global_deps)
- $(call quiet,CC $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@
-
-.deps/%.d: %.c $(global_deps)
- @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
- $(CC) -M $(CPPFLAGS) $(FINAL_CFLAGS) $< > $@.$$$$ 2>/dev/null ; \
- sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
- rm -f $@.$$$$
-
-.deps/%.d: %.cc $(global_deps)
- @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
- $(CXX) -M $(CPPFLAGS) $(FINAL_CXXFLAGS) $< > $@.$$$$ 2>/dev/null ; \
- sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
- rm -f $@.$$$$
+ @mkdir -p .deps/$(@D)
+ $(call quiet,CC $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@ -MD -MP -MF .deps/$*.d
.PHONY : clean
clean:
@@ -301,7 +291,6 @@ notmuch_client_srcs = \
notmuch-tag.c \
notmuch-time.c \
query-string.c \
- show-message.c \
mime-node.c \
json.c
diff --git a/NEWS b/NEWS
index 2e393c4b..fb55efb7 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,153 @@
+Notmuch 0.13.2 (2012-06-02)
+===========================
+
+Bug-fix release
+---------------
+
+Update contrib/notmuch-deliver for API changes in 0.13. This fixes a
+compilation error for this contrib package.
+
+Notmuch 0.13.1 (2012-05-29)
+===========================
+
+Bug-fix release
+---------------
+
+Fix inserting of UTF-8 characters from *text/plain* parts in reply
+
+ While notmuch gained ability to insert content from other than *text/plain*
+ parts of email whenever *text/plain* parts are not available (notably
+ HTML-only emails), replying to mails that do have *text/plain* the
+ non-ASCII characters were incorrectly decoded. This is now fixed.
+
+`notmuch_database_get_directory` and
+`notmuch_database_find_message_by_filename` now work on read-only
+databases
+
+ Previously, these functions attempted to create directory documents
+ that didn't exist and would return an error or abort when given a
+ read-only database. Now they no longer create directory documents
+ and simply return a `NULL` object if the directory does not exist,
+ as documented.
+
+Fix compilation of ruby bindings.
+
+ Revert to dynamic linking, since the statically linked bindings did
+ not work well.
+
+Notmuch 0.13 (2012-05-15)
+=========================
+
+Command-Line Interface
+----------------------
+
+JSON reply format
+
+ `notmuch reply` can now produce JSON output that contains the headers
+ for a reply message and full information about the original message
+ begin replied to. This allows MUAs to create replies intelligently.
+ For example, an MUA that can parse HTML might quote HTML parts.
+
+ Calling notmuch reply with `--format=json` imposes the restriction that
+ only a single message is returned by the search, as replying to
+ multiple messages does not have a well-defined behavior. The default
+ retains its current behavior for multiple message replies.
+
+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
+
+Raw show format changes
+
+ The output of show `--format=raw` has changed for multipart and
+ message parts. Previously, the output was a mash of somewhat-parsed
+ headers and transfer-decoded bodies. Now, such parts are reproduced
+ faithfully from the original source. Message parts (which includes
+ part 0) output the full message, including the message headers (but
+ not the transfer headers). Multipart parts output the part as
+ encoded in the original message, including the part's headers. Leaf
+ parts, as before, output the part's transfer-decoded body.
+
+Listing configuration items
+
+ The new `config list` command prints out all configuration items and
+ their values.
+
+Emacs Interface
+---------------
+
+Changes to tagging interface
+
+ The user-facing tagging functions in the Emacs interface have been
+ normalized across all notmuch modes. The tagging functions are now
+ notmuch-search-tag in search-mode, and notmuch-show-tag in
+ show-mode. They accept a string representing a single tag change,
+ or a list of tag changes. See 'M-x describe-function notmuch-tag'
+ for more information.
+
+ NOTE: This breaks compatibility with old tagging functions, so user
+ may need to update in custom configurations.
+
+Reply improvement using the JSON format
+
+ Emacs now uses the JSON reply format to create replies. It obeys
+ the customization variables message-citation-line-format and
+ message-citation-line-function when creating the first line of the
+ reply body, and it will quote HTML parts if no text/plain parts are
+ available.
+
+New add-on tool: notmuch-mutt
+-----------------------------
+
+The new contrib/ tool `notmuch-mutt` provides Notmuch integration for
+the Mutt mail user agent. Using it, Mutt users can perform mail
+search, thread reconstruction, and mail tagging/untagging without
+leaving Mutt. notmuch-mutt, formerly distributed under the name
+`mutt-notmuch` by Stefano Zacchiroli, will be maintained as a notmuch
+contrib/ from now on.
+
+Library changes
+---------------
+
+The API changes detailed below break binary and source compatibility,
+so libnotmuch has been bumped to version 3.0.0.
+
+The function `notmuch_database_close` has been split into
+`notmuch_database_close` and `notmuch_database_destroy`
+
+ This makes it possible for long running programs to close the xapian
+ database and thus release the lock associated with it without
+ destroying the data structures obtained from it.
+
+`notmuch_database_open`, `notmuch_database_create`, and
+`notmuch_database_get_directory` now return errors
+
+ The type signatures of these functions have changed so that the
+ functions now return a `notmuch_status_t` and take an out-argument for
+ returning the new database object or directory object.
+
+Go bindings changes
+-------------------
+
+Go 1 compatibility
+
+ The go bindings and the `notmuch-addrlookup` utility are now
+ compatible with go 1.
+
Notmuch 0.12 (2012-03-20)
=========================
@@ -6,25 +156,25 @@ Command-Line Interface
Reply to sender
- "notmuch reply" has gained the ability to create a reply template
+ `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).
+ `--reply-to=(all|sender)`.
Mail store folder/file ignore
- A new configuration option, `new.ignore`, lets users specify a
- ;-separated list of file and directory names that will not be
- searched for messages by "notmuch new".
+ A new configuration option, `new.ignore`, lets users specify a
+ ;-separated list of file and directory names that will not be
+ searched for messages by `notmuch new`.
- NOTE: *Every* file/directory that goes by one of those names will
- be ignored, independent of its depth/location in the mail store.
+ NOTE: *Every* file/directory that goes by one of those names will
+ be ignored, independent of its depth/location in the mail store.
Unified help and manual pages
- The notmuch help command now runs man for the appropriate page. If
- you install notmuch somewhere "unusual", you may need to update
- MANPATH.
+ The notmuch help command now runs man for the appropriate page. If
+ you install notmuch somewhere "unusual", you may need to update
+ MANPATH.
Manual page for notmuch configuration options
@@ -44,26 +194,26 @@ Reply to sender
More flexible and consistent tagging operations
- All tagging operations ("+", "-", "*") now accept multiple tags with
- "+" or "-" prefix, like "*" operation in notmuch-search view before.
+ All tagging operations ('+', '-', '*') now accept multiple tags with
+ '+' or '-' prefix, like '*' operation in notmuch-search view before.
- "*" operation (`notmuch-show-tag-all') is now available in
+ '*' operation (`notmuch-show-tag-all`) is now available in
notmuch-show view.
- `Notmuch-show-{add,remove}-tag' functions no longer accept tag
- argument, `notmuch-show-tag-message' should be used instead. Custom
+ `notmuch-show-{add,remove}-tag` functions no longer accept tag
+ argument, `notmuch-show-tag-message` should be used instead. Custom
bindings using these functions should be updated, e.g.:
- (notmuch-show-remove-tag "unread")
+ (notmuch-show-remove-tag "unread")
should be changed to:
- (notmuch-show-tag-message "-unread")
+ (notmuch-show-tag-message "-unread")
Refreshing the show view ('=' by default) no longer opens or closes messages
To get the old behavior of putting messages back in their initial
- opened/closed state, use a prefix argument, e.g., C-u =.
+ opened/closed state, use a prefix argument, e.g., 'C-u ='.
Attachment buttons can be used to view or save attachments.
@@ -76,14 +226,14 @@ Attachment buttons can be used to view or save attachments.
New functions
- `notmuch-show-stash-mlarchive-link{,-and-go}' allow stashing and
+ `notmuch-show-stash-mlarchive-link{,-and-go}` allow stashing and
optionally visiting a URI to the current message at one of a number
of Mailing List Archives.
Fix MML tag quoting in replies
The MML tag quoting fix of 0.11.1 unintentionally quoted tags
- inserted in `message-setup-hook'. Quoting is now limited to the
+ inserted in `message-setup-hook`. Quoting is now limited to the
cited message.
Show view archiving key binding changes
@@ -107,9 +257,9 @@ Generate inline patch fake attachment file names from message subject
'git format-patch' instead of just "inline patch". See "Notmuch Show
Insert Text/Plain Hook" in the notmuch customize interface.
-Enable `notmuch-search-line-faces' by default
+Enable `notmuch-search-line-faces` by default
- Make the `notmuch-search-line-faces' functionality more discoverable
+ Make the `notmuch-search-line-faces` functionality more discoverable
for new users by showing "unread" messages bold and "flagged"
messages blue by default in the search view.
@@ -123,7 +273,7 @@ Library changes
New functions
- notmuch_query_add_tag_exclude supports the new tag exclusion
+ `notmuch_query_add_tag_exclude` supports the new tag exclusion
feature.
Python bindings changes
@@ -153,10 +303,10 @@ Compatibility with GMime 2.6
Notmuch 0.11.1 (2012-02-03)
===========================
-Bug-fix release.
-----------------
+Bug-fix release
+---------------
-Fix error handling in python bindings.
+Fix error handling in python bindings
The python bindings in 0.11 failed to detect NULL pointers being
returned from libnotmuch functions and thus failed to raise
@@ -183,13 +333,13 @@ Command-Line Interface
Hooks
Hooks have been introduced to notmuch. Hooks are scripts that notmuch
- invokes before and after certain actions. Initially, "notmuch new"
- supports "pre-new" and "post-new" hooks that are run before and after
+ invokes before and after certain actions. Initially, `notmuch new`
+ supports `pre-new` and `post-new` hooks that are run before and after
importing new messages into the database.
-notmuch reply --decrypt bugfix
+`notmuch reply --decrypt bugfix`
- The "notmuch reply" command with --decrypt argument had a rarely
+ The `notmuch reply` command with `--decrypt` argument had a rarely
occurring bug that caused an encrypted message not to be decrypted
sometimes. This is now fixed.
@@ -198,7 +348,7 @@ Performance
Automatic tag query optimization
- "notmuch tag" now automatically optimizes the user's query to
+ `notmuch tag` now automatically optimizes the user's query to
exclude messages whose tags won't change. In the past, we've
suggested that people do this by hand; this is no longer necessary.
@@ -206,7 +356,7 @@ Don't sort messages when creating a dump file
This speeds up tag dumps considerably, without any loss of
information. To replicate the old behavior of sorted output (for
- example to compare two dump files), one can use e.g. sort(1).
+ example to compare two dump files), one can use e.g. `sort(1)`.
Memory Management
-----------------
@@ -226,11 +376,11 @@ Bug fixes
should have scrolled down to show more of the current message instead.
This is now fixed.
-Support "notmuch new" as a notmuch-poll-script
+Support `notmuch new` as a notmuch-poll-script
- It's now possible to use "notmuch new" as a notmuch-poll-script
+ It's now possible to use `notmuch new` as a notmuch-poll-script
directly. This is also the new default. This allows taking better
- advantage of the "notmuch new" hooks from emacs without intermediate
+ advantage of the `notmuch new` hooks from emacs without intermediate
scripts.
Improvements in saved search management
@@ -259,7 +409,7 @@ Use space as default thousands separator
changed by customizing "notmuch-hello-thousands-separator".
Call notmuch-show instead of notmuch-search when clicking on
-buttonized id: links.
+buttonized id: links
New function notmuch-show-advance
@@ -267,39 +417,39 @@ New function notmuch-show-advance
less invasive than notmuch-show-advance-and-archive. It can easily
be bound to SPC with:
- (define-key notmuch-show-mode-map " " 'notmuch-show-advance)
+ (define-key notmuch-show-mode-map " " 'notmuch-show-advance)
-Various performance improvements.
+Various performance improvements
New add-on tool
---------------
-The tool contrib/notmuch-deliver helps with initial delivery and
-tagging of mail (replacing running notmuch new).
+The tool `contrib/notmuch-deliver` helps with initial delivery and
+tagging of mail (replacing running `notmuch new`).
Notmuch 0.10.2 (2011-12-04)
===========================
-Bug-fix release.
-----------------
+Bug-fix release
+---------------
-Fix crash in python bindings.
+Fix crash in python bindings
- The python bindings did not call g_type_init, which caused crashes
+ The python bindings did not call `g_type_init`, which caused crashes
for some, but not all users.
Notmuch 0.10.1 (2011-11-25)
===========================
-Bug-fix release.
-----------------
+Bug-fix release
+---------------
-Fix --help argument
+Fix `--help` argument
- Argument processing changes in 0.10 introduced a bug where "notmuch
- --help" crashed while "notmuch help" worked fine. This is fixed in
- 0.10.1.
+ Argument processing changes in 0.10 introduced a bug where
+ `notmuch --help` crashed while `notmuch help` worked fine.
+ This is fixed in 0.10.1.
Notmuch 0.10 (2011-11-23)
=========================
@@ -307,7 +457,7 @@ Notmuch 0.10 (2011-11-23)
New build and testing features
------------------------------
-Emacs tests are now done in dtach. This means that dtach is now
+Emacs tests are now done in `dtach`. This means that dtach is now
needed to run the notmuch test suite, at least until the checking for
prerequisites is improved.
@@ -316,32 +466,33 @@ Full test coverage of the stashing feature in Emacs.
New command-line features
-------------------------
-Add "notmuch restore --accumulate" option
+Add `notmuch restore --accumulate` option
- The --accumulate switch causes the union of the existing and new tags to be
- applied, instead of replacing each message's tags as they are read in from
- the dump file.
+ The `--accumulate` switch causes the union of the existing and new tags to
+ be applied, instead of replacing each message's tags as they are read in
+ from the dump file.
-Add search terms to "notmuch dump"
+Add search terms to `notmuch dump`
The dump command now takes an optional search term much like notmuch
search/show/tag. The output file argument of dump is deprecated in
favour of using stdout.
-Add "notmuch search" --offset and --limit options
+Add `notmuch search` `--offset` and `--limit` options
- The search command now takes options --offset=[-]N and --limit=N to limit
- the number of results shown.
+ The search command now takes options `--offset=[-]N` and `--limit=N` to
+ limit the number of results shown.
-Add "notmuch count --output" option
+Add `notmuch count --output` option
The count command is now capable of counting threads in addition to
- messages. This is selected using the new --output=(threads|messages) option.
+ messages. This is selected using the new `--output=(threads|messages)`
+ option.
New emacs UI features
---------------------
-Add tab-completion for notmuch-search and notmuch-search-filter
+Add tab-completion for `notmuch-search` and `notmuch-search-filter`
These functions now support completion tags for query parts
starting with "tag:".
@@ -355,7 +506,7 @@ Add keybinding ('c I') for stashing Message-ID's without an id: prefix
Reduces manual labour when stashing them for use outside notmuch.
-Do not query on notmuch-search exit
+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.
@@ -372,10 +523,10 @@ Search avoids opening and parsing message files
improve search speed by as much as 10X, but taking advantage of this
requires a database rebuild:
- notmuch dump > notmuch.dump
- # Backup, then remove notmuch database ($MAIL/.notmuch)
- notmuch new
- notmuch restore notmuch.dump
+ notmuch dump > notmuch.dump
+ # Backup, then remove notmuch database ($MAIL/.notmuch)
+ notmuch new
+ notmuch restore notmuch.dump
New collection of add-on tools
------------------------------
@@ -397,9 +548,9 @@ Notmuch 0.9 (2011-10-01)
New, general features
---------------------
-Correct handling of interruptions during "notmuch new"
+Correct handling of interruptions during `notmuch new`
- "notmuch new" now operates as a series of small, self-consistent
+ `notmuch new` now operates as a series of small, self-consistent
transactions, so it can correctly resume after an interruption or
crash. Previously, interruption could lose existing tags, fail to
detect messages on resume, or leave the database in a state
@@ -410,14 +561,14 @@ Library changes
New functions
- notmuch_database_begin_atomic and notmuch_database_end_atomic allow
- multiple database operations to be performed atomically.
+ `notmuch_database_begin_atomic` and `notmuch_database_end_atomic`
+ allow multiple database operations to be performed atomically.
- notmuch_database_find_message_by_filename does exactly what it says.
+ `notmuch_database_find_message_by_filename` does exactly what it says.
API changes
- notmuch_database_find_message (and n_d_f_m_by_filename) now return
+ `notmuch_database_find_message` (and `n_d_f_m_by_filename`) now return
a status indicator and uses an output parameter for the
message. This change required changing the SONAME of libnotmuch to
libnotmuch.so.2
@@ -427,33 +578,33 @@ Python bindings changes
- Re-encode python unicode objects to utf-8 before passing back to
libnotmuch.
- - Support Database().begin_atomic()/end_atomic()
- - Support Database().find_message_by_filename()
+ - Support `Database().begin_atomic()/end_atomic()`
+ - Support `Database().find_message_by_filename()`
NB! This needs a db opened in READ-WRITE mode currently, or it will crash
the python process. The is a limitation (=bug) of the underlying libnotmuch.
- Fixes where we would not throw NotmuchErrors when we should (Justus Winter)
- - Update for n_d_find_message* API changes (see above).
+ - Update for `n_d_find_message*` API changes (see above).
Ruby bindings changes
---------------------
- - Wrap new library functions notmuch_database_{begin,end}_atomic.
- - Add new exception Notmuch::UnbalancedAtomicError.
- - Rename destroy to destroy! according to Ruby naming conventions.
- - Update for n_d_find_message* API changes (see above).
+ - Wrap new library functions `notmuch_database_{begin,end}_atomic.`
+ - Add new exception `Notmuch::UnbalancedAtomicError.`
+ - Rename destroy to destroy! according to Ruby naming conventions.
+ - Update for `n_d_find_message*` API changes (see above).
Emacs improvements
------------------
* Add gpg callback to crypto sigstatus buttons to retrieve/refresh
signing key.
- * Add notmuch-show-refresh-view function (and corresponding binding)
+ * Add `notmuch-show-refresh-view` function (and corresponding binding)
to refresh the view of a notmuch-show buffer.
Reply formatting cleanup
------------------------
- "notmuch reply" no longer includes notification that non-leafnode
+ `notmuch reply` no longer includes notification that non-leafnode
MIME parts are being suppressed.
Notmuch 0.8 (2011-09-10)
@@ -480,7 +631,7 @@ Documentation update for Ruby bindings
Unicode, iterator, PEP8 changes for python bindings
- PEP8 (code formatting) changes for python files.
- - Remove Tags.__len__ ; see 0.6 release notes for motivation.
+ - Remove `Tags.__len__` ; see 0.6 release notes for motivation.
- Decode headers as UTF8, encode (unicode) database paths as UTF8.
Notmuch 0.7 (2011-08-01)
@@ -511,7 +662,7 @@ Sebastian Spaeth contributed two changes related to unicode and UTF8:
* query string is encoded as a UTF8 byte string
Build-System improvements
-------------------------
+-------------------------
Generate notmuch.sym after the relevant object files
@@ -521,31 +672,33 @@ Generate notmuch.sym after the relevant object files
Notmuch 0.6.1 (2011-07-17)
==========================
-Bug-fix release.
-----------------
+Bug-fix release
+---------------
-Re-export Xapian exception typeinfo symbols.
+Re-export Xapian exception typeinfo symbols
It turned out our aggressive symbol hiding caused problems for
people running gcc 4.4.5.
Notmuch 0.6 (2011-07-01)
=======================
+
New, general features
---------------------
+
Folder-based searching
Notmuch queries can now include a search term to match the
directories in which mail files are stored (within the mail
storage). The syntax is as follows:
- folder:<path>
+ folder:<path>
For example, one might use things such as:
- folder:spam
- folder:2011-*
- folder:work/todo
+ folder:spam
+ folder:2011-*
+ folder:work/todo
to match any path containing a directory "spam", "work/todo", or
containing a directory starting with "2011-", respectively.
@@ -560,14 +713,14 @@ Folder-based searching
this feature for all mail, the entire notmuch index will need to be
rebuilt as follows:
- notmuch dump > notmuch.dump
- # Backup, then remove notmuch database ($MAIL/.notmuch)
- notmuch new
- notmuch restore notmuch.dump
+ notmuch dump > notmuch.dump
+ # Backup, then remove notmuch database ($MAIL/.notmuch)
+ notmuch new
+ notmuch restore notmuch.dump
Support for PGP/MIME
- Both the command line interface and the emacs-interface have new
+ Both the command-line interface and the emacs-interface have new
support for PGP/MIME, detailed below. Thanks to Daniel Kahn Gillmor
and Jameson Graef Rollins for making this happen.
@@ -581,6 +734,7 @@ New, automatic tags: "signed" and "encrypted"
New command-line features
-------------------------
+
Add new "notmuch show --verify" option for signature verification
This option instruct notmuch to verify the signature of
@@ -608,7 +762,7 @@ Add new "notmuch show --part" option
part, (such as --format=json for extracting a message part with JSON
formatting).
-Deprecate "notmuch search-tags", (in favor of "notmuch search --output=tags *")
+Deprecate "notmuch search-tags" (in favor of "notmuch search --output=tags *")
The "notmuch search-tags" sub-command has been redundant since the
addition of the --output=tags option to "notmuch search". We now
@@ -618,6 +772,7 @@ Deprecate "notmuch search-tags", (in favor of "notmuch search --output=tags *")
Performance improvements
------------------------
+
Faster searches (by doing fewer searches to construct threads)
Whenever a user asks for search results as threads, notmuch first
@@ -660,7 +815,7 @@ Support for PGP/MIME (GnuPG)
messages. Automatically display decrypted content for
multipart/encrypted messages. See the emacs variable
notmuch-crypto-process-mime for more information. Note that this
- needs gpg-agent and a pinentry tool just as the command line tools.
+ needs gpg-agent and a pinentry tool just as the command-line tools.
Also note there is no support SMIME yet.
Output of pipe command is now displayed if pipe command fails
@@ -695,7 +850,7 @@ Automatic detection and hiding of original message in top-posted message
When a message contains a line looking something like:
- ----- Original Message -----
+ ----- Original Message -----
emacs hides this and all subsequent lines as an "original message",
(allowing the user to click or press enter on the "original message"
@@ -710,8 +865,8 @@ New hooks for running code when tags are modified
tool. To facilitate this, two new hooks are added which can be
modified in the following settings of the notmuch customize group:
- Notmuch Before Tag Hook
- Notmuch After Tag Hook
+ Notmuch Before Tag Hook
+ Notmuch After Tag Hook
New optional support for hiding some multipart/alternative parts
@@ -719,7 +874,7 @@ New optional support for hiding some multipart/alternative parts
multipart/alternative group (such as a text/plain part as well as a
text/html part). Users can configure the setting:
- Notmuch Show All Multipart/Alternative Parts
+ Notmuch Show All Multipart/Alternative Parts
to "off" in the notmuch customize group to have the interface
automatically hide some part alternatives (such as text/html
@@ -736,11 +891,14 @@ Avoid getting confused by Subject and Author fields with newline characters
Replacing all characters with ASCII code less than 32 with a question mark.
-Cleaner display of From line in email messages (remove double quotes,
-and drop "name" if it's actually just a repeat of the email address).
+Cleaner display of From line in email messages
+
+ Remove double quotes, and drop "name" if it's actually just a repeat of
+ the email address.
Vim interface improvements
--------------------------
+
Felipe Contreras provided a number of updates for the vim interface:
* Using sendmail directly rather than mailx,
@@ -751,47 +909,54 @@ Felipe Contreras provided a number of updates for the vim interface:
Bindings improvements
---------------------
+
Ruby bindings are now much more complete
- Including QUERY.sort, QUERY.to_s, MESSAGE.maildir_flags_to_tags,
- MESSAGE.tags_to_maildir_flags, and MESSAGE.get_filenames
+ Including `QUERY.sort`, `QUERY.to_s`, `MESSAGE.maildir_flags_to_tags`,
+ `MESSAGE.tags_to_maildir_flags`, and `MESSAGE.get_filenames`
+
+Python bindings have been updated and extended
-* Python bindings have been updated and extended
(docs online at http://packages.python.org/notmuch/)
New bindings:
- - Message().get_filenames(),
- - Message().tags_to_maildir_flags(),Message().maildir_flags_to_tags()
- - list(Threads()) and list(Messages) works now
- - Message().__cmp__() and __hash__()
+ - `Message().get_filenames()`, `Message().tags_to_maildir_flags()`,
+ `Message().maildir_flags_to_tags()`, `list(Threads())` and
+ `list(Messages)` works now
+ - `Message().__cmp__()` and `__hash__()`
+
These allow, for example:
- if msg1 == msg2: ...
- As well as set arithmetic on Messages():
+ if msg1 == msg2: ...
- s1, s2= set(msgs1), set(msgs2)
- s1.union(s2)
- s2 -= s1
+ As well as set arithmetic on `Messages()`:
+
+ s1, s2 = set(msgs1), set(msgs2)
+ s1.union(s2)
+ s2 -= s1
Removed:
- - len(Messages()) as it exhausted the iterator.
- Use len(list(Messages())) or
- Query.count_messages() to get the length.
+
+ - `len(Messages())` as it exhausted the iterator
+
+ Use `len(list(Messages()))` or `Query.count_messages()`
+ to get the length.
Added initial Go bindings in bindings/go
New build-system features
-------------------------
+
Added support for building in a directory other than the source directory
This can be used with the widely-supported idiom of simply running
the configure script from some other directory:
- mkdir build
- cd build
- ../configure
- make
+ mkdir build
+ cd build
+ ../configure
+ make
Fix to save configure options for future, implicit runs of configure
@@ -803,7 +968,8 @@ Fix to save configure options for future, implicit runs of configure
New test-suite feature
----------------------
-Binary for bash for running test suite now located via PATH.
+
+Binary for bash for running test suite now located via PATH
The notmuch test suite requires a fairly recent version of bash (>=
bash 4). As some systems supply an older version of bash at
@@ -812,12 +978,12 @@ Binary for bash for running test suite now located via PATH.
simply install bash >= 4 somewhere on $PATH before /bin and then use
the test suite.
-Support for testing output with a trailing newline.
+Support for testing output with a trailing newline
Previously, some tests would fail to notice a difference in the
presence/absence of a trailing newline in a program output, (which
has led to bugs in the past). Now, carefully-written tests (using
- test_expect_equal_file rather than test_expect_equal) will detect
+ `test_expect_equal_file` rather than `test_expect_equal`) will detect
any change in the presence/absence of a trailing newline. Many tests
are updated to take advantage of this.
@@ -831,6 +997,7 @@ Avoiding accessing user's $HOME while running test suite
General bug fixes
-----------------
+
Output *all* files for "notmuch search --output=files"
For the cases where multiple files have the same Message ID,
@@ -841,11 +1008,11 @@ Fixed spurious search results from "overlapped" indexing of addresses
This fixed a bug where a search for:
- to:user@elsewhere.com
+ to:user@elsewhere.com
would incorrectly match a message sent:
- To: user@example,com, someone@elsewhere.com
+ To: user@example,com, someone@elsewhere.com
Fix --output=json when search has no results
@@ -854,15 +1021,15 @@ Fix --output=json when search has no results
return a valid json object representing an empty array "[]" as
expected.
-fix the automatic detection of the From address for "notmuch reply"
-from the Received headers in some cases.
+Fix the automatic detection of the From address for "notmuch reply"
+from the Received headers in some cases
Fix core dump on DragonFlyBSD due to -1 return value from
-sysconf(_SC_GETPW_R_SIZE_MAX).
+`sysconf(_SC_GETPW_R_SIZE_MAX)`
Cleaned up several memory leaks
-Eliminated a few, rare segmentation faults and a double-free.
+Eliminated a few, rare segmentation faults and a double-free
Fix libnotmuch library to only export notmuch API functions
@@ -872,6 +1039,7 @@ Fix libnotmuch library to only export notmuch API functions
Emacs-interface bug fixes
-------------------------
+
Display any unexpected output or errors from "notmuch search" invocations
Previously any misformatted output or trailing error messages were
@@ -899,21 +1067,23 @@ Fix hiding of a message when a previously-hidden citation is visible
Notmuch 0.5 (2010-11-11)
========================
+
New, general features
---------------------
+
Maildir-flag synchronization
Notmuch now knows how to synchronize flags in maildir filenames with
tags in the notmuch database. The following flag/tag mappings are
supported:
- Flag <-> Tag
- ---- -----
- 'D' draft
- 'F' flagged
- 'P' passed
- 'R' replied
- 'S' unread (added when 'S' flag is not present)
+ Flag <-> Tag
+ ---- -----
+ 'D' draft
+ 'F' flagged
+ 'P' passed
+ 'R' replied
+ 'S' unread (added when 'S' flag is not present)
The synchronization occurs in both directions, (for example, adding
the 'S' flag to a file will cause the "unread" tag to be added, and
@@ -923,10 +1093,10 @@ Maildir-flag synchronization
This synchronization is enabled by default for users of the
command-line interface, (though only files in directories named
"cur" or "new" will be renamed). It can be disabled by setting the
- new maildir.synchronize_flags option in the configuration file. For
+ new `maildir.synchronize_flags` option in the configuration file. For
example:
- notmuch config set maildir.synchronize_flags false
+ notmuch config set maildir.synchronize_flags false
Users upgrading may also want to run "notmuch setup" once (just
accept the existing configuration) to get a new, nicely-commented
@@ -935,8 +1105,8 @@ Maildir-flag synchronization
For users of the notmuch library, the new synchronization
functionality is available with the following two new functions:
- notmuch_message_maildir_flags_to_tags
- notmuch_message_tags_to_maildir_flags
+ notmuch_message_maildir_flags_to_tags
+ notmuch_message_tags_to_maildir_flags
It is anticipated that future improvements to this support will
allow for safe synchronization of the 'T' flag with the "deleted"
@@ -944,24 +1114,26 @@ Maildir-flag synchronization
New library features
--------------------
+
Support for querying multiple filenames for a single message
It is common for the mailstore to contain multiple files with the
same message ID. Previously, notmuch would always hide these
duplicate files, (returning a single, arbitrary filename with
- notmuch_message_get_filename).
+ `notmuch_message_get_filename`).
With this release, library users can access all filenames for a
message with the new function:
- notmuch_message_get_filenames
+ notmuch_message_get_filenames
- Together with notmuch_filenames_valid, notmuch_filenames_get, and
- notmuch_filenames_move_to_next it is now possible to iterate over
- all available filenames for a given message.
+ Together with `notmuch_filenames_valid`, `notmuch_filenames_get`,
+ and `notmuch_filenames_move_to_next` it is now possible to iterate
+ over all available filenames for a given message.
New command-line features
-------------------------
+
New "notmuch show --format=raw" for getting at original email contents
This new feature allows for a fully-functional email client to be
@@ -973,7 +1145,7 @@ New "notmuch show --format=raw" for getting at original email contents
do this, simply set the notmuch-command variable in emacs to the
name of a script containing:
- ssh user@host notmuch "$@"
+ ssh user@host notmuch "$@"
If the ssh client has enabled connection sharing (ControlMaster
option in OpenSSH), the emacs interface can be quite responsive this
@@ -981,11 +1153,12 @@ New "notmuch show --format=raw" for getting at original email contents
General bug fixes
-----------------
+
Fix "notmuch search" to print nothing when nothing matches
The 0.4 release had a bug in which:
- notmuch search <expression-with-no-matches>
+ notmuch search <expression-with-no-matches>
would produce a single blank line of output, (where previous
versions would produce no output. This fix also causes a change in
@@ -994,17 +1167,18 @@ Fix "notmuch search" to print nothing when nothing matches
Emacs interface improvements
----------------------------
+
Fix to allow pipe ('|') command to work when using notmuch over ssh
-Fix count of lines in hidden signatures.
+Fix count of lines in hidden signatures
-Omit repeated subject lines in (collapsed) thread display.
+Omit repeated subject lines in (collapsed) thread display
-Display current thread subject in a header line.
+Display current thread subject in a header line
-Provide a "c i" binding to copy a thread ID from the search view.
+Provide a "c i" binding to copy a thread ID from the search view
-Allow for notmuch-fcc-dirs to have a value of nil.
+Allow for notmuch-fcc-dirs to have a value of nil
Also, the more complex form of notmuch-fcc-dirs now has a slightly
different format. It no longer has a special first-element, fallback
@@ -1016,7 +1190,8 @@ Allow for notmuch-fcc-dirs to have a value of nil.
Vim interface improvements
--------------------------
-Felipe Contreras provided a number of updates for the vim interface.
+
+Felipe Contreras provided a number of updates for the vim interface
These include optimizations, support for newer versions of vim, fixed
support for sending mail on modern systems, new commands, and
@@ -1024,13 +1199,16 @@ Felipe Contreras provided a number of updates for the vim interface.
New bindings
------------
+
Added initial ruby bindings in bindings/ruby
Notmuch 0.4 (2010-11-01)
========================
+
New command-line features
-------------------------
-notmuch search --output=(summary|threads|messages|tags|files)
+
+`notmuch search --output=(summary|threads|messages|tags|files)`
This new option allows for particular items to be returned from
notmuch searches. The "summary" option is the default and behaves
@@ -1041,11 +1219,11 @@ notmuch search --output=(summary|threads|messages|tags|files)
expected that this new option will be very useful in shell
scripts. For example:
- for file in $(notmuch search --output=files <search-terms>); do
- <operations-on> "$file"
- done
+ for file in $(notmuch search --output=files <search-terms>); do
+ <operations-on> "$file"
+ done
-notmuch show --format=mbox <search-specification>
+`notmuch show --format=mbox <search-specification>`
This new option allows for the messages matching a search
specification to be presented as an mbox. Specifically the "mboxrd"
@@ -1054,7 +1232,7 @@ notmuch show --format=mbox <search-specification>
beginning of all lines beginning with one or more '>' characters
followed by the 5 characters "From ".
-notmuch config [get|set] <section>.<item> [value ...]
+`notmuch config [get|set] <section>.<item> [value ...]`
The new top-level "config" command allows for any value in the
notmuch configuration file to be queried or set to a new value. Both
@@ -1070,13 +1248,15 @@ Avoid setting Bcc header in "notmuch reply"
New library features
--------------------
-Add notmuch_query_get_query_string and notmuch_query_get_sort
+
+Add `notmuch_query_get_query_string` and `notmuch_query_get_sort`
These are simply functions for querying properties of a
- notmuch_query_t object.
+ `notmuch_query_t` object.
New emacs features
------------------
+
Enable Fcc of all sent messages by default (to "sent" directory)
All messages sent from the emacs interface will now be saved to the
@@ -1117,7 +1297,7 @@ Allow search-result color specifications to overlay each other
case in previous releases). See "Notmuch Search Line Faces" in the
notmuch customize interface.
-Make hidden author names still available for incremental search.
+Make hidden author names still available for incremental search
When there is insufficient space to display all authors of a thread
in search results, the names of hidden authors are now still made
@@ -1134,6 +1314,7 @@ New binding of Control-TAB (works like TAB in reverse)
New build-system features
-------------------------
+
Various portability fixes have been applied
These include fixes for build failures on at least Solaris, FreeBSD,
@@ -1146,18 +1327,18 @@ Arrange for libnotmuch to be found automatically after make install
errors of the form "libnotmuch.so could not be found" immediately
after installing. This support takes two forms:
- 1. If the library is installed to a system directory,
- (configured in /etc/ld.so.conf), then "make install" will
- automatically run ldconfig.
+ 1. If the library is installed to a system directory,
+ (configured in /etc/ld.so.conf), then "make install" will
+ automatically run ldconfig.
- 2. If the library is installed to a non-system directory, the
- build system adds a DR_RUNPATH entry to the final binary
- pointing to the directory to which the library is installed.
+ 2. If the library is installed to a non-system directory, the
+ build system adds a `DR_RUNPATH` entry to the final binary
+ pointing to the directory to which the library is installed.
When this support works, the user should be able to run notmuch
immediately after "make install", without any errors trying to find
the notmuch library, and without having to manually set environment
- variables such as LD_LIBRARY_PATH.
+ variables such as `LD_LIBRARY_PATH`.
Check compiler/linker options before using them
@@ -1168,7 +1349,8 @@ Check compiler/linker options before using them
New test-suite features
-----------------------
-New modularization of test suite.
+
+New modularization of test suite
Thanks to a gracious relicensing of the test-suite infrastructure
from the git project, notmuch now has a modular test suite. This
@@ -1179,7 +1361,7 @@ New modularization of test suite.
it easy to run the test suite within valgrind (pass --valgrind to
notmuch-test or to any sub-script) which has been very useful.
-New testing of emacs interface.
+New testing of emacs interface
The test suite has been augmented to allow automated testing of the
emacs interfaces. So far, this includes basic searches, display of
@@ -1190,18 +1372,19 @@ New testing of emacs interface.
General bug fixes
-----------------
-Fix potential corruption of database when "notmuch new " is interrupted.
+
+Fix potential corruption of database when "notmuch new" is interrupted
Previously, an interruption of "notmuch new" would (rarely) result
in a corrupt database. The corruption would manifest itself by a
persistent error of the form:
- document ID of 1234 has no thread ID
+ document ID of 1234 has no thread ID
The message-adding code has been carefully audited and reworked to
avoid this sort of corruption regardless of when it is interrupted.
-Fix failure with extremely long message ID headers.
+Fix failure with extremely long message ID headers
Previously, a message with an extremely long message ID, (say, more
than 300 characters), would fail to be added to notmuch, (triggering
@@ -1213,9 +1396,10 @@ Fix for messages with "charset=unknown-8bit"
GMime warning, (which would then trip up emacs or other interfaces
parsing the notmuch results).
-Fix notmuch_query_search_threads function to return NULL on any exception
+Fix `notmuch_query_search_threads` function to return NULL on any exception
-Fix "notmuch search" to return non-zero if notmuch_query_search_threads fails
+Fix "notmuch search" to return non-zero if `notmuch_query_search_threads`
+fails
Previously, this command could confusingly report a Xapian
exception, yet still return an error code of 0. It now correctly
@@ -1223,6 +1407,7 @@ Fix "notmuch search" to return non-zero if notmuch_query_search_threads fails
Emacs bug fixes
---------------
+
Fix to handle a message with a subject containing, for example "[1234]"
Previously, a message subject containing a sequence of digits within
@@ -1239,11 +1424,13 @@ Fix to correctly handle message IDs containing ".."
Python-binding fixes
--------------------
+
The python bindings for notmuch have been updated to work with python3.
Debian-specific fixes
---------------------
-Fix emacs initialization so "M-x notmuch" works for users by default.
+
+Fix emacs initialization so "M-x notmuch" works for users by default
Now, a new Debian user can immediately run "emacs -f notmuch" after
"apt-get install notmuch". Previously, the user would have had to
@@ -1252,8 +1439,10 @@ Fix emacs initialization so "M-x notmuch" works for users by default.
Notmuch 0.3.1 (2010-04-27)
==========================
+
General bug fixes
-----------------
+
Fix an infinite loop in "notmuch reply"
This bug could be triggered by replying to a message where the
@@ -1269,22 +1458,26 @@ Fix a potential SEGV in "notmuch search"
Emacs bug fixes
---------------
-Fix calculations for line wrapping in the primary "notmuch" view.
+
+Fix calculations for line wrapping in the primary "notmuch" view
Fix Fcc support to prompt to create a directory if the specified Fcc
-directory does not exist.
+directory does not exist
Build fix
---------
-Fix build on OpenSolaris (at least) due to missing 'extern "C"' block.
+
+Fix build on OpenSolaris (at least) due to missing 'extern "C"' block
Without this, the C++ sources could not find strcasestr and the
final linking of notmuch would fail.
Notmuch 0.3 (2010-04-27)
========================
+
New command-line features
-------------------------
+
User-configurable tags for new messages
A new "new.tags" option is available in the configuration file to
@@ -1323,6 +1516,7 @@ Indication of author names that match a search
New: Python bindings
--------------------
+
Sebastian Spaeth has contributed his python bindings for the notmuch
library to the central repository. These bindings were previously
known as "cnotmuch" within python but have now been renamed to be
@@ -1339,6 +1533,7 @@ package-building scripts. Improvements are welcome.
Emacs interface improvements
----------------------------
+
An entirely new initial view for notmuch, (friendly yet powerful)
Some of us call the new view "notmuch hello" but you can get at it
@@ -1355,16 +1550,16 @@ An entirely new initial view for notmuch, (friendly yet powerful)
search of messages with that tag that's simply a click (or keypress)
away.
- Note: For users that liked the original mode of "emacs -f notmuch"
- immediately displaying a particular search result, we
- recommend instead running something like:
+ NOTE: For users that liked the original mode of "emacs -f notmuch"
+ immediately displaying a particular search result, we recommend
+ instead running something like:
- emacs --eval '(notmuch search "tag:inbox" t)'
+ emacs --eval '(notmuch search "tag:inbox" t)'
- The "t" means to sort the messages in an "oldest first" order,
- (as notmuch would do previously by default). You can also
- leave that off to have your search results in "newest first"
- order.
+ The "t" means to sort the messages in an "oldest first" order,
+ (as notmuch would do previously by default). You can also
+ leave that off to have your search results in "newest first"
+ order.
Full-featured "customize" support for configuring notmuch
@@ -1395,7 +1590,7 @@ Support for doing tab-completion of email addresses
One such program (implemented in python with the python bindings to
notmuch) is available via:
- git clone http://jkr.acm.jhu.edu/git/notmuch_addresses.git
+ git clone http://jkr.acm.jhu.edu/git/notmuch_addresses.git
Install that program as notmuch-addresses on your PATH, and then
hitting TAB on a partial email address or name within the To: or Cc:
@@ -1418,7 +1613,7 @@ New 'G' key binding to trigger mail refresh (G == "Get new mail")
typically invoke "notmuch new" and then perhaps several "notmuch
tag" commands.
-Implement emacs message display with the JSON output from notmuch.
+Implement emacs message display with the JSON output from notmuch
This is much more robust than the previous implementation, (where
some HTML mails and mail quoting the notmuch code with the delimiter
@@ -1480,15 +1675,16 @@ Customizable formatting of search results
the various fields in a "notmuch search" buffer. See the "Notmuch
Search Result Format" section of the customize interface.
-Generate nicer names for search buffers when using a saved search.
+Generate nicer names for search buffers when using a saved search
-Add a notmuch User-Agent header when sending mail from notmuch/emacs.
+Add a notmuch User-Agent header when sending mail from notmuch/emacs
-New keybinding (M-Ret) to open all collapsed messages in a thread.
+New keybinding (M-Ret) to open all collapsed messages in a thread
New library feature
-------------------
-Provide a new NOTMUCH_SORT_UNSORTED value for queries
+
+Provide a new `NOTMUCH_SORT_UNSORTED` value for queries
This can be somewhat faster when sorting simply isn't desired. For
example when collecting a set of messages that will all be
@@ -1498,27 +1694,30 @@ Provide a new NOTMUCH_SORT_UNSORTED value for queries
Build fixes
-----------
+
Fix to compile against GMime 2.6
Previously notmuch insisted on being able to find GMime 2.4, (even
though GMime 2.6 would have worked all along).
-Fix configure script to accept (and ignore) various standard options.
+Fix configure script to accept (and ignore) various standard options
For example, those that the Gentoo build scripts expect configure to
accept are now all accepted.
Test suite
----------
-A large number of new tests for the many new features.
-Better display of output from failed tests.
+A large number of new tests for the many new features
+
+Better display of output from failed tests
Now shows failures with diff rather than forcing the user to gaze at
complete actual and expected output looking for deviation.
Notmuch 0.2 (2010-04-16)
========================
+
This is the second release of the notmuch mail system, with actual
detailed release notes this time!
@@ -1534,7 +1733,8 @@ notmuch in subsequent releases.
General features
----------------
-Better guessing of From: header.
+
+Better guessing of From: header
Notmuch now tries harder to guess which configured address should be
used as the From: line in a "notmuch reply". It will examine the
@@ -1548,7 +1748,7 @@ Make "notmuch count" with no arguments count all messages
Previously, it was hard to construct a search term that was
guaranteed to match all messages.
-Provide a new special-case search term of "*" to match all messages.
+Provide a new special-case search term of "*" to match all messages
This can be used in any command accepting a search term, such as
"notmuch search '*'". Note that you'll want to take care that the
@@ -1558,7 +1758,7 @@ Provide a new special-case search term of "*" to match all messages.
other search terms.
Automatically detect thread connections even when a parent message is
-missing.
+missing
Previously, if two or more message were received with a common
parent, but that parent was not received, then these messages would
@@ -1567,6 +1767,7 @@ missing.
General bug fixes
-----------------
+
Fix potential data loss in "notmuch new" with SIGINT
One code path in "notmuch new" was not properly handling
@@ -1574,7 +1775,7 @@ Fix potential data loss in "notmuch new" with SIGINT
the database (and their tags being lost) if the user pressed
Control-C while "notmuch new" was working.
-Fix segfault when a message includes a MIME part that is empty.
+Fix segfault when a message includes a MIME part that is empty
Fix handling of non-ASCII characters with --format=json
@@ -1586,11 +1787,11 @@ Fix headers to be properly decoded in "notmuch reply"
Previously, the user might see:
- Subject: Re: =?iso-8859-2?q?Rozlu=E8ka?=
+ Subject: Re: =?iso-8859-2?q?Rozlu=E8ka?=
rather than:
- Subject: Re: Rozlučka
+ Subject: Re: Rozlučka
The former text is properly encoded to be RFC-compliant SMTP, will
be sent correctly, and will be properly decoded by the
@@ -1599,7 +1800,8 @@ Fix headers to be properly decoded in "notmuch reply"
Emacs client features
---------------------
-Show the last few lines of citations as well as the first few lines.
+
+Show the last few lines of citations as well as the first few lines
It's often the case that the last sentence of a citation is what is
being replied to directly, so the last few lines are often much more
@@ -1608,20 +1810,20 @@ Show the last few lines of citations as well as the first few lines.
notmuch-show-citation-lines-suffix).
The '+' and '-' commands in the search view can now add and remove
-tags by region.
+tags by region
Selective bulk tagging is now possible by selecting a region of
threads and then using either the '+' or '-' keybindings. Bulk
tagging is still available for all threads matching the current
search with the '*' binding.
-More meaningful buffer names for thread-view buffers.
+More meaningful buffer names for thread-view buffers
Notmuch now uses the Subject of the thread as the buffer
name. Previously it was using the thread ID, which is a meaningless
number to the user.
-Provide for customized colors of threads in search view based on tags.
+Provide for customized colors of threads in search view based on tags
See the documentation of notmuch-search-line-faces, (or us "M-x
customize" and browse to the "notmuch" group within "Applications"
@@ -1629,33 +1831,35 @@ Provide for customized colors of threads in search view based on tags.
Build-system features
---------------------
-Add support to properly build libnotmuch on Darwin systems (OS X).
-Add support to configure for many standard options.
+Add support to properly build libnotmuch on Darwin systems (OS X)
+
+Add support to configure for many standard options
We include actual support for:
- --includedir --mandir --sysconfdir
+ --includedir --mandir --sysconfdir
And accept and silently ignore several more:
- --build --infodir --libexecdir --localstatedir
- --disable-maintainer-mode --disable-dependency-tracking
+ --build --infodir --libexecdir --localstatedir
+ --disable-maintainer-mode --disable-dependency-tracking
Install emacs client in "make install" rather than requiring a
-separate "make install-emacs".
+separate "make install-emacs"
-Automatically compute versions numbers between releases.
+Automatically compute versions numbers between releases
This support uses the git-describe notation, so a version such as
0.1-144-g43cbbfc indicates a version that is 144 commits since the
0.1 release and is available as git commit "43cbbfc".
-Add a new "make test" target to run the test suite and actually verify
-its results.
+Add a new "make test" target to run the test suite and actually
+verify its results
Notmuch 0.1 (2010-04-05)
========================
+
This is the first release of the notmuch mail system.
It includes the libnotmuch library, the notmuch command-line
@@ -1667,3 +1871,13 @@ a performance bug that made notmuch very slow when modifying
tags. This would cause distracting pauses when reading mail while
notmuch would wait for Xapian when removing the "inbox" and "unread"
tags from messages in a thread.
+
+
+<!--
+ Local variables:
+ mode: text
+ tab-width: 8
+ indent-tabs-mode: nil
+ End:
+ vi: sw=8 ts=8 et
+-->
diff --git a/bindings/go/Makefile b/bindings/go/Makefile
index aba2d595..c38f2340 100644
--- a/bindings/go/Makefile
+++ b/bindings/go/Makefile
@@ -1,30 +1,40 @@
-# Copyright 2009 The Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
+# Makefile for the go bindings of notmuch
-include ${GOROOT}/src/Make.inc
+export GOPATH ?= $(shell pwd)
+export CGO_CFLAGS ?= -I../../../../lib
+export CGO_LDFLAGS ?= -L../../../../lib
-all: install
+GO ?= go
+GOFMT ?= gofmt
-DIRS=\
- pkg\
- cmds\
+all: notmuch notmuch-addrlookup
+.PHONY: notmuch
+notmuch:
+ $(GO) install notmuch
-clean.dirs: $(addsuffix .clean, $(DIRS))
-install.dirs: $(addsuffix .install, $(DIRS))
-nuke.dirs: $(addsuffix .nuke, $(DIRS))
-test.dirs: $(addsuffix .test, $(TEST))
-bench.dirs: $(addsuffix .bench, $(BENCH))
+.PHONY: goconfig
+goconfig:
+ if [ ! -d src/github.com/kless/goconfig/config ]; then \
+ $(GO) get github.com/kless/goconfig/config; \
+ fi
-%.clean:
- +cd $* && $(QUOTED_GOBIN)/gomake clean
+.PHONY: notmuch-addrlookup
+notmuch-addrlookup: notmuch goconfig
+ $(GO) install notmuch-addrlookup
-%.install:
- +cd $* && $(QUOTED_GOBIN)/gomake install
+.PHONY: format
+format:
+ $(GOFMT) -w=true $(GOFMT_OPTS) src/notmuch
+ $(GOFMT) -w=true $(GOFMT_OPTS) src/notmuch-addrlookup
-clean: clean.dirs
+.PHONY: check-format
+check-format:
+ $(GOFMT) -d=true $(GOFMT_OPTS) src/notmuch
+ $(GOFMT) -d=true $(GOFMT_OPTS) src/notmuch-addrlookup
-install: install.dirs
-
-#-include ${GOROOT}/src/Make.deps
+.PHONY: clean
+clean:
+ $(GO) clean notmuch
+ $(GO) clean notmuch-addrlookup
+ rm -rf pkg bin
diff --git a/bindings/go/cmds/Makefile b/bindings/go/cmds/Makefile
deleted file mode 100644
index afbc6d22..00000000
--- a/bindings/go/cmds/Makefile
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright 2009 The Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-include ${GOROOT}/src/Make.inc
-
-TARG=notmuch-addrlookup
-GOFILES=\
- notmuch-addrlookup.go
-
-include ${GOROOT}/src/Make.cmd
diff --git a/bindings/go/pkg/Makefile b/bindings/go/pkg/Makefile
deleted file mode 100644
index de89dbc9..00000000
--- a/bindings/go/pkg/Makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright 2009 The Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-include $(GOROOT)/src/Make.inc
-
-TARG=notmuch
-CGOFILES=notmuch.go
-CGO_LDFLAGS=-lnotmuch
-
-CLEANFILES+=notmuch_test
-
-include $(GOROOT)/src/Make.pkg
-
-%: install %.go
- $(GC) $*.go
- $(LD) -o $@ $*.$O
diff --git a/bindings/go/cmds/notmuch-addrlookup.go b/bindings/go/src/notmuch-addrlookup/addrlookup.go
index 16958e50..59283f81 100644
--- a/bindings/go/cmds/notmuch-addrlookup.go
+++ b/bindings/go/src/notmuch-addrlookup/addrlookup.go
@@ -22,18 +22,18 @@ type frequencies map[string]uint
/* Used to sort the email addresses from most to least used */
func sort_by_freq(m1, m2 *mail_addr_freq) int {
- if (m1.count[0] == m2.count[0] &&
+ if m1.count[0] == m2.count[0] &&
m1.count[1] == m2.count[1] &&
- m1.count[2] == m2.count[2]) {
+ m1.count[2] == m2.count[2] {
return 0
}
- if (m1.count[0] > m2.count[0] ||
+ if m1.count[0] > m2.count[0] ||
m1.count[0] == m2.count[0] &&
- m1.count[1] > m2.count[1] ||
+ m1.count[1] > m2.count[1] ||
m1.count[0] == m2.count[0] &&
- m1.count[1] == m2.count[1] &&
- m1.count[2] > m2.count[2]) {
+ m1.count[1] == m2.count[1] &&
+ m1.count[2] > m2.count[2] {
return -1
}
@@ -46,17 +46,17 @@ func (self *maddresses) Len() int {
return len(*self)
}
-func (self *maddresses) Less(i,j int) bool {
+func (self *maddresses) Less(i, j int) bool {
m1 := (*self)[i]
m2 := (*self)[j]
- v := sort_by_freq(m1, m2)
- if v<=0 {
+ v := sort_by_freq(m1, m2)
+ if v <= 0 {
return true
}
return false
}
-func (self *maddresses) Swap(i,j int) {
+func (self *maddresses) Swap(i, j int) {
(*self)[i], (*self)[j] = (*self)[j], (*self)[i]
}
@@ -66,7 +66,7 @@ func frequent_fullname(freqs frequencies) string {
fullname := ""
freqs_sz := len(freqs)
- for mail,freq := range freqs {
+ for mail, freq := range freqs {
if (freq > maxfreq && mail != "") || freqs_sz == 1 {
// only use the entry if it has a real name
// or if this is the only entry
@@ -86,33 +86,33 @@ func addresses_by_frequency(msgs *notmuch.Messages, name string, pass uint, addr
// "<?(?P<mail>\\b\\w+([-+.]\\w+)*\\@\\w+[-\\.\\w]*\\.([-\\.\\w]+)*\\w\\b)>?)"
pattern = `.*` + strings.ToLower(name) + `.*`
var re *regexp.Regexp = nil
- var err os.Error = nil
- if re,err = regexp.Compile(pattern); err != nil {
+ var err error = nil
+ if re, err = regexp.Compile(pattern); err != nil {
log.Printf("error: %v\n", err)
return &freqs
}
-
+
headers := []string{"from"}
if pass == 1 {
headers = append(headers, "to", "cc", "bcc")
}
- for ;msgs.Valid();msgs.MoveToNext() {
+ for ; msgs.Valid(); msgs.MoveToNext() {
msg := msgs.Get()
//println("==> msg [", msg.GetMessageId(), "]")
- for _,header := range headers {
+ for _, header := range headers {
froms := strings.ToLower(msg.GetHeader(header))
//println(" froms: ["+froms+"]")
- for _,from := range strings.Split(froms, ",", -1) {
+ for _, from := range strings.Split(froms, ",") {
from = strings.Trim(from, " ")
match := re.FindString(from)
//println(" -> match: ["+match+"]")
- occ,ok := freqs[match]
+ occ, ok := freqs[match]
if !ok {
freqs[match] = 0
occ = 0
}
- freqs[match] = occ+1
+ freqs[match] = occ + 1
}
}
}
@@ -125,7 +125,7 @@ func search_address_passes(queries [3]*notmuch.Query, name string) []string {
addr_to_realname := make(map[string]*frequencies)
var pass uint = 0 // 0-based
- for _,query := range queries {
+ for _, query := range queries {
if query == nil {
//println("**warning: idx [",idx,"] contains a nil query")
continue
@@ -133,9 +133,9 @@ func search_address_passes(queries [3]*notmuch.Query, name string) []string {
msgs := query.SearchMessages()
ht := addresses_by_frequency(msgs, name, pass, &addr_to_realname)
for addr, count := range *ht {
- freq,ok := addr_freq[addr]
+ freq, ok := addr_freq[addr]
if !ok {
- freq = &mail_addr_freq{addr:addr, count:[3]uint{0,0,0}}
+ freq = &mail_addr_freq{addr: addr, count: [3]uint{0, 0, 0}}
}
freq.count[pass] = count
addr_freq[addr] = freq
@@ -154,8 +154,8 @@ func search_address_passes(queries [3]*notmuch.Query, name string) []string {
}
sort.Sort(&addrs)
- for _,addr := range addrs {
- freqs,ok := addr_to_realname[addr.addr]
+ for _, addr := range addrs {
+ freqs, ok := addr_to_realname[addr.addr]
if ok {
val = append(val, frequent_fullname(*freqs))
} else {
@@ -179,7 +179,7 @@ type address_matcher struct {
func new_address_matcher() *address_matcher {
var cfg *config.Config
- var err os.Error
+ var err error
// honor NOTMUCH_CONFIG
home := os.Getenv("NOTMUCH_CONFIG")
@@ -187,30 +187,34 @@ func new_address_matcher() *address_matcher {
home = os.Getenv("HOME")
}
- if cfg,err = config.ReadDefault(path.Join(home, ".notmuch-config")); err != nil {
- log.Fatalf("error loading config file:",err)
+ if cfg, err = config.ReadDefault(path.Join(home, ".notmuch-config")); err != nil {
+ log.Fatalf("error loading config file:", err)
}
- db_path,_ := cfg.String("database", "path")
- primary_email,_ := cfg.String("user", "primary_email")
- addrbook_tag,err := cfg.String("user", "addrbook_tag")
+ db_path, _ := cfg.String("database", "path")
+ primary_email, _ := cfg.String("user", "primary_email")
+ addrbook_tag, err := cfg.String("user", "addrbook_tag")
if err != nil {
addrbook_tag = "addressbook"
}
- self := &address_matcher{db:nil,
- user_db_path:db_path,
- user_primary_email:primary_email,
- user_addrbook_tag:addrbook_tag}
+ self := &address_matcher{db: nil,
+ user_db_path: db_path,
+ user_primary_email: primary_email,
+ user_addrbook_tag: addrbook_tag}
return self
}
func (self *address_matcher) run(name string) {
queries := [3]*notmuch.Query{}
-
+
// open the database
- self.db = notmuch.OpenDatabase(self.user_db_path,
- notmuch.DATABASE_MODE_READ_ONLY)
+ if db, status := notmuch.OpenDatabase(self.user_db_path,
+ notmuch.DATABASE_MODE_READ_ONLY); status == notmuch.STATUS_SUCCESS {
+ self.db = db
+ } else {
+ log.Fatalf("Failed to open the database: %v\n", status)
+ }
// pass 1: look at all from: addresses with the address book tag
query := "tag:" + self.user_addrbook_tag
@@ -222,7 +226,7 @@ func (self *address_matcher) run(name string) {
// pass 2: look at all to: addresses sent from our primary mail
query = ""
if name != "" {
- query = "to:"+name+"*"
+ query = "to:" + name + "*"
}
if self.user_primary_email != "" {
query = query + " from:" + self.user_primary_email
@@ -230,17 +234,17 @@ func (self *address_matcher) run(name string) {
queries[1] = self.db.CreateQuery(query)
// if that leads only to a few hits, we check every from too
- if queries[0].CountMessages() + queries[1].CountMessages() < 10 {
+ if queries[0].CountMessages()+queries[1].CountMessages() < 10 {
query = ""
if name != "" {
- query = "from:"+name+"*"
+ query = "from:" + name + "*"
}
queries[2] = self.db.CreateQuery(query)
}
-
+
// actually retrieve and sort addresses
results := search_address_passes(queries, name)
- for _,v := range results {
+ for _, v := range results {
if v != "" && v != "\n" {
fmt.Println(v)
}
@@ -256,4 +260,4 @@ func main() {
name = os.Args[1]
}
app.run(name)
-} \ No newline at end of file
+}
diff --git a/bindings/go/pkg/notmuch.go b/bindings/go/src/notmuch/notmuch.go
index c6844ef9..00bd53ac 100644
--- a/bindings/go/pkg/notmuch.go
+++ b/bindings/go/src/notmuch/notmuch.go
@@ -3,6 +3,8 @@
package notmuch
/*
+#cgo LDFLAGS: -lnotmuch
+
#include <stdlib.h>
#include <string.h>
#include <time.h>
@@ -13,24 +15,26 @@ import "unsafe"
// Status codes used for the return values of most functions
type Status C.notmuch_status_t
+
const (
- STATUS_SUCCESS Status = 0
+ STATUS_SUCCESS Status = iota
STATUS_OUT_OF_MEMORY
- STATUS_READ_ONLY_DATABASE
- STATUS_XAPIAN_EXCEPTION
- STATUS_FILE_ERROR
- STATUS_FILE_NOT_EMAIL
- STATUS_DUPLICATE_MESSAGE_ID
- STATUS_NULL_POINTER
- STATUS_TAG_TOO_LONG
- STATUS_UNBALANCED_FREEZE_THAW
+ STATUS_READ_ONLY_DATABASE
+ STATUS_XAPIAN_EXCEPTION
+ STATUS_FILE_ERROR
+ STATUS_FILE_NOT_EMAIL
+ STATUS_DUPLICATE_MESSAGE_ID
+ STATUS_NULL_POINTER
+ STATUS_TAG_TOO_LONG
+ STATUS_UNBALANCED_FREEZE_THAW
+ STATUS_UNBALANCED_ATOMIC
- STATUS_LAST_STATUS
+ STATUS_LAST_STATUS
)
func (self Status) String() string {
var p *C.char
-
+
// p is read-only
p = C.notmuch_status_to_string(C.notmuch_status_t(self))
if p != nil {
@@ -80,27 +84,28 @@ type Filenames struct {
}
type DatabaseMode C.notmuch_database_mode_t
+
const (
- DATABASE_MODE_READ_ONLY DatabaseMode = 0
- DATABASE_MODE_READ_WRITE
+ DATABASE_MODE_READ_ONLY DatabaseMode = 0
+ DATABASE_MODE_READ_WRITE
)
// Create a new, empty notmuch database located at 'path'
-func NewDatabase(path string) *Database {
+func NewDatabase(path string) (*Database, Status) {
var c_path *C.char = C.CString(path)
defer C.free(unsafe.Pointer(c_path))
if c_path == nil {
- return nil
+ return nil, STATUS_OUT_OF_MEMORY
}
- self := &Database{db:nil}
- self.db = C.notmuch_database_create(c_path)
- if self.db == nil {
- return nil
+ self := &Database{db: nil}
+ st := Status(C.notmuch_database_create(c_path, &self.db))
+ if st != STATUS_SUCCESS {
+ return nil, st
}
- return self
+ return self, st
}
/* Open an existing notmuch database located at 'path'.
@@ -114,41 +119,41 @@ func NewDatabase(path string) *Database {
* An existing notmuch database can be identified by the presence of a
* directory named ".notmuch" below 'path'.
*
- * The caller should call notmuch_database_close when finished with
+ * The caller should call notmuch_database_destroy when finished with
* this database.
*
* In case of any failure, this function returns NULL, (after printing
* an error message on stderr).
*/
-func OpenDatabase(path string, mode DatabaseMode) *Database {
+func OpenDatabase(path string, mode DatabaseMode) (*Database, Status) {
var c_path *C.char = C.CString(path)
defer C.free(unsafe.Pointer(c_path))
if c_path == nil {
- return nil
+ return nil, STATUS_OUT_OF_MEMORY
}
- self := &Database{db:nil}
- self.db = C.notmuch_database_open(c_path, C.notmuch_database_mode_t(mode))
- if self.db == nil {
- return nil
+ self := &Database{db: nil}
+ st := Status(C.notmuch_database_open(c_path, C.notmuch_database_mode_t(mode), &self.db))
+ if st != STATUS_SUCCESS {
+ return nil, st
}
- return self
+ return self, st
}
/* Close the given notmuch database, freeing all associated
* resources. See notmuch_database_open. */
func (self *Database) Close() {
- C.notmuch_database_close(self.db)
+ C.notmuch_database_destroy(self.db)
}
/* Return the database path of the given database.
*/
func (self *Database) GetPath() string {
-
- /* The return value is a string owned by notmuch so should not be
- * modified nor freed by the caller. */
+
+ /* The return value is a string owned by notmuch so should not be
+ * modified nor freed by the caller. */
var p *C.char = C.notmuch_database_get_path(self.db)
if p != nil {
s := C.GoString(p)
@@ -178,7 +183,6 @@ func (self *Database) NeedsUpgrade() bool {
// TODO: notmuch_database_upgrade
-
/* Retrieve a directory object from the database for 'path'.
*
* Here, 'path' should be a path relative to the path of 'database'
@@ -187,19 +191,20 @@ func (self *Database) NeedsUpgrade() bool {
*
* Can return NULL if a Xapian exception occurs.
*/
-func (self *Database) GetDirectory(path string) *Directory {
+func (self *Database) GetDirectory(path string) (*Directory, Status) {
var c_path *C.char = C.CString(path)
defer C.free(unsafe.Pointer(c_path))
if c_path == nil {
- return nil
+ return nil, STATUS_OUT_OF_MEMORY
}
- c_dir := C.notmuch_database_get_directory(self.db, c_path)
- if c_dir == nil {
- return nil
+ var c_dir *C.notmuch_directory_t
+ st := Status(C.notmuch_database_get_directory(self.db, c_path, &c_dir))
+ if st != STATUS_SUCCESS || c_dir == nil {
+ return nil, st
}
- return &Directory{dir:c_dir}
+ return &Directory{dir: c_dir}, st
}
/* Add a new message to the given notmuch database.
@@ -242,8 +247,7 @@ func (self *Database) GetDirectory(path string) *Directory {
* NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
* mode so no message can be added.
*/
-func
-(self *Database) AddMessage(fname string) (*Message, Status) {
+func (self *Database) AddMessage(fname string) (*Message, Status) {
var c_fname *C.char = C.CString(fname)
defer C.free(unsafe.Pointer(c_fname))
@@ -254,7 +258,7 @@ func
var c_msg *C.notmuch_message_t = new(C.notmuch_message_t)
st := Status(C.notmuch_database_add_message(self.db, c_fname, &c_msg))
- return &Message{message:c_msg}, st
+ return &Message{message: c_msg}, st
}
/* Remove a message from the given notmuch database.
@@ -282,7 +286,7 @@ func
* mode so no message can be removed.
*/
func (self *Database) RemoveMessage(fname string) Status {
-
+
var c_fname *C.char = C.CString(fname)
defer C.free(unsafe.Pointer(c_fname))
@@ -306,20 +310,21 @@ func (self *Database) RemoveMessage(fname string) Status {
* * An out-of-memory situation occurs
* * A Xapian exception occurs
*/
-func (self *Database) FindMessage(message_id string) *Message {
-
+func (self *Database) FindMessage(message_id string) (*Message, Status) {
+
var c_msg_id *C.char = C.CString(message_id)
defer C.free(unsafe.Pointer(c_msg_id))
if c_msg_id == nil {
- return nil
+ return nil, STATUS_OUT_OF_MEMORY
}
- msg := C.notmuch_database_find_message(self.db, c_msg_id)
- if msg == nil {
- return nil
+ msg := &Message{message: nil}
+ st := Status(C.notmuch_database_find_message(self.db, c_msg_id, &msg.message))
+ if st != STATUS_SUCCESS {
+ return nil, st
}
- return &Message{message:msg}
+ return msg, st
}
/* Return a list of all tags found in the database.
@@ -334,7 +339,7 @@ func (self *Database) GetAllTags() *Tags {
if tags == nil {
return nil
}
- return &Tags{tags:tags}
+ return &Tags{tags: tags}
}
/* Create a new query for 'database'.
@@ -362,7 +367,7 @@ func (self *Database) GetAllTags() *Tags {
* Will return NULL if insufficient memory is available.
*/
func (self *Database) CreateQuery(query string) *Query {
-
+
var c_query *C.char = C.CString(query)
defer C.free(unsafe.Pointer(c_query))
@@ -374,11 +379,12 @@ func (self *Database) CreateQuery(query string) *Query {
if q == nil {
return nil
}
- return &Query{query:q}
+ return &Query{query: q}
}
/* Sort values for notmuch_query_set_sort */
type Sort C.notmuch_sort_t
+
const (
SORT_OLDEST_FIRST Sort = 0
SORT_NEWEST_FIRST
@@ -391,7 +397,7 @@ func (self *Query) String() string {
// FIXME: do we own 'q' or not ?
q := C.notmuch_query_get_query_string(self.query)
//defer C.free(unsafe.Pointer(q))
-
+
if q != nil {
s := C.GoString(q)
return s
@@ -453,7 +459,7 @@ func (self *Query) SearchThreads() *Threads {
if threads == nil {
return nil
}
- return &Threads{threads:threads}
+ return &Threads{threads: threads}
}
/* Execute a query for messages, returning a notmuch_messages_t object
@@ -499,7 +505,7 @@ func (self *Query) SearchMessages() *Messages {
if msgs == nil {
return nil
}
- return &Messages{messages:msgs}
+ return &Messages{messages: msgs}
}
/* Destroy a notmuch_query_t along with any associated resources.
@@ -601,7 +607,7 @@ func (self *Messages) Get() *Message {
if msg == nil {
return nil
}
- return &Message{message:msg}
+ return &Message{message: msg}
}
/* Move the 'messages' iterator to the next message.
@@ -653,7 +659,7 @@ func (self *Messages) CollectTags() *Tags {
if tags == nil {
return nil
}
- return &Tags{tags:tags}
+ return &Tags{tags: tags}
}
/* Get the message ID of 'message'.
@@ -693,14 +699,14 @@ func (self *Message) GetMessageId() string {
* message belongs to a single thread.
*/
func (self *Message) GetThreadId() string {
-
+
if self.message == nil {
return ""
}
id := C.notmuch_message_get_thread_id(self.message)
// we dont own id
// defer C.free(unsafe.Pointer(id))
-
+
if id == nil {
return ""
}
@@ -733,7 +739,7 @@ func (self *Message) GetReplies() *Messages {
if msgs == nil {
return nil
}
- return &Messages{messages:msgs}
+ return &Messages{messages: msgs}
}
/* Get a filename for the email corresponding to 'message'.
@@ -757,7 +763,7 @@ func (self *Message) GetFileName() string {
fname := C.notmuch_message_get_filename(self.message)
// we dont own fname
// defer C.free(unsafe.Pointer(fname))
-
+
if fname == nil {
return ""
}
@@ -766,6 +772,7 @@ func (self *Message) GetFileName() string {
}
type Flag C.notmuch_message_flag_t
+
const (
MESSAGE_FLAG_MATCH Flag = 0
)
@@ -812,16 +819,16 @@ func (self *Message) GetHeader(header string) string {
if self.message == nil {
return ""
}
-
+
var c_header *C.char = C.CString(header)
defer C.free(unsafe.Pointer(c_header))
-
+
/* we dont own value */
value := C.notmuch_message_get_header(self.message, c_header)
if value == nil {
return ""
}
-
+
return C.GoString(value)
}
@@ -863,7 +870,7 @@ func (self *Message) GetTags() *Tags {
if tags == nil {
return nil
}
- return &Tags{tags:tags}
+ return &Tags{tags: tags}
}
/* The longest possible tag value. */
@@ -1120,4 +1127,5 @@ func (self *Filenames) Destroy() {
}
C.notmuch_filenames_destroy(self.fnames)
}
+
/* EOF */
diff --git a/bindings/python/docs/source/database.rst b/bindings/python/docs/source/database.rst
index ee71085f..2464bfff 100644
--- a/bindings/python/docs/source/database.rst
+++ b/bindings/python/docs/source/database.rst
@@ -9,6 +9,8 @@
.. automethod:: open(path, status=MODE.READ_ONLY)
+ .. automethod:: close
+
.. automethod:: get_path
.. automethod:: get_version
diff --git a/bindings/python/docs/source/filesystem.rst b/bindings/python/docs/source/filesystem.rst
index 685dc4d3..4eb78107 100644
--- a/bindings/python/docs/source/filesystem.rst
+++ b/bindings/python/docs/source/filesystem.rst
@@ -10,8 +10,6 @@ Files and directories
.. automethod:: Filenames.__len__
- .. automethod:: Filenames.as_generator
-
:class:`Directoy` -- A directory entry in the database
------------------------------------------------------
diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py
index 44d40fdb..e5c74cfb 100644
--- a/bindings/python/notmuch/database.py
+++ b/bindings/python/notmuch/database.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
import os
@@ -56,21 +56,14 @@ class Database(object):
:class:`Database` objects implement the context manager protocol
so you can use the :keyword:`with` statement to ensure that the
- database is properly closed.
+ database is properly closed. See :meth:`close` for more
+ information.
.. note::
Any function in this class can and will throw an
:exc:`NotInitializedError` if the database was not intitialized
properly.
-
- .. note::
-
- Do remember that as soon as we tear down (e.g. via `del db`) this
- object, all underlying derived objects such as queries, threads,
- messages, tags etc will be freed by the underlying library as well.
- Accessing these objects will lead to segfaults and other unexpected
- behavior. See above for more details.
"""
_std_db_path = None
"""Class attribute to cache user's default database"""
@@ -80,8 +73,8 @@ class Database(object):
"""notmuch_database_get_directory"""
_get_directory = nmlib.notmuch_database_get_directory
- _get_directory.argtypes = [NotmuchDatabaseP, c_char_p]
- _get_directory.restype = NotmuchDirectoryP
+ _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
+ _get_directory.restype = c_uint
"""notmuch_database_get_path"""
_get_path = nmlib.notmuch_database_get_path
@@ -95,8 +88,8 @@ class Database(object):
"""notmuch_database_open"""
_open = nmlib.notmuch_database_open
- _open.argtypes = [c_char_p, c_uint]
- _open.restype = NotmuchDatabaseP
+ _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
+ _open.restype = c_uint
"""notmuch_database_upgrade"""
_upgrade = nmlib.notmuch_database_upgrade
@@ -122,8 +115,8 @@ class Database(object):
"""notmuch_database_create"""
_create = nmlib.notmuch_database_create
- _create.argtypes = [c_char_p]
- _create.restype = NotmuchDatabaseP
+ _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
+ _create.restype = c_uint
def __init__(self, path = None, create = False,
mode = MODE.READ_ONLY):
@@ -161,8 +154,13 @@ class Database(object):
else:
self.create(path)
+ _destroy = nmlib.notmuch_database_destroy
+ _destroy.argtypes = [NotmuchDatabaseP]
+ _destroy.restype = None
+
def __del__(self):
- self.close()
+ if self._db:
+ self._destroy(self._db)
def _assert_db_is_initialized(self):
"""Raises :exc:`NotInitializedError` if self._db is `None`"""
@@ -184,16 +182,17 @@ class Database(object):
:raises: :exc:`NotmuchError` in case of any failure
(possibly after printing an error message on stderr).
"""
- if self._db is not None:
+ if self._db:
raise NotmuchError(message="Cannot create db, this Database() "
"already has an open one.")
- res = Database._create(_str(path), Database.MODE.READ_WRITE)
+ db = NotmuchDatabaseP()
+ status = Database._create(_str(path), Database.MODE.READ_WRITE, byref(db))
- if not res:
- raise NotmuchError(
- message="Could not create the specified database")
- self._db = res
+ if status != STATUS.SUCCESS:
+ raise NotmuchError(status)
+ self._db = db
+ return status
def open(self, path, mode=0):
"""Opens an existing database
@@ -207,21 +206,31 @@ class Database(object):
:raises: Raises :exc:`NotmuchError` in case of any failure
(possibly after printing an error message on stderr).
"""
- res = Database._open(_str(path), mode)
+ db = NotmuchDatabaseP()
+ status = Database._open(_str(path), mode, byref(db))
- if not res:
- raise NotmuchError(message="Could not open the specified database")
- self._db = res
+ if status != STATUS.SUCCESS:
+ raise NotmuchError(status)
+ self._db = db
+ return status
_close = nmlib.notmuch_database_close
_close.argtypes = [NotmuchDatabaseP]
_close.restype = None
def close(self):
- """Close and free the notmuch database if needed"""
- if self._db is not None:
+ '''
+ Closes the notmuch database.
+
+ .. warning::
+
+ This function closes the notmuch database. From that point
+ on every method invoked on any object ever derived from
+ the closed database may cease to function and raise a
+ NotmuchError.
+ '''
+ if self._db:
self._close(self._db)
- self._db = None
def __enter__(self):
'''
@@ -337,7 +346,6 @@ class Database(object):
def get_directory(self, path):
"""Returns a :class:`Directory` of path,
- (creating it if it does not exist(?))
:param path: An unicode string containing the path relative to the path
of database (see :meth:`get_path`), or else should be an absolute
@@ -345,18 +353,9 @@ class Database(object):
:returns: :class:`Directory` or raises an exception.
:raises: :exc:`FileError` if path is not relative database or absolute
with initial components same as database.
- :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
- opened in read-write mode
"""
self._assert_db_is_initialized()
- # work around libnotmuch calling exit(3), see
- # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
- # TODO: remove once this issue is resolved
- if self.mode != Database.MODE.READ_WRITE:
- raise ReadOnlyDatabaseError('The database has to be opened in '
- 'read-write mode for get_directory')
-
# sanity checking if path is valid, and make path absolute
if path and path[0] == os.sep:
# we got an absolute path
@@ -369,7 +368,13 @@ class Database(object):
#we got a relative path, make it absolute
abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
- dir_p = Database._get_directory(self._db, _str(path))
+ dir_p = NotmuchDirectoryP()
+ status = Database._get_directory(self._db, _str(path), byref(dir_p))
+
+ if status != STATUS.SUCCESS:
+ raise NotmuchError(status)
+ if not dir_p:
+ return None
# return the Directory, init it with the absolute path
return Directory(abs_dirpath, dir_p, self)
@@ -521,19 +526,10 @@ class Database(object):
retry.
:raises: :exc:`NotInitializedError` if the database was not
intitialized.
- :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
- opened in read-write mode
*Added in notmuch 0.9*"""
self._assert_db_is_initialized()
- # work around libnotmuch calling exit(3), see
- # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
- # TODO: remove once this issue is resolved
- if self.mode != Database.MODE.READ_WRITE:
- raise ReadOnlyDatabaseError('The database has to be opened in '
- 'read-write mode for get_directory')
-
msg_p = NotmuchMessageP()
status = Database._find_message_by_filename(self._db, _str(filename),
byref(msg_p))
diff --git a/bindings/python/notmuch/directory.py b/bindings/python/notmuch/directory.py
index 284cbdce..ae115f81 100644
--- a/bindings/python/notmuch/directory.py
+++ b/bindings/python/notmuch/directory.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_uint, c_long
@@ -181,5 +181,5 @@ class Directory(object):
def __del__(self):
"""Close and free the Directory"""
- if self._dir_p is not None:
+ if self._dir_p:
self._destroy(self._dir_p)
diff --git a/bindings/python/notmuch/filenames.py b/bindings/python/notmuch/filenames.py
index 12050df9..a0b29563 100644
--- a/bindings/python/notmuch/filenames.py
+++ b/bindings/python/notmuch/filenames.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p
from notmuch.globals import (
@@ -32,29 +32,32 @@ from .errors import (
class Filenames(Python3StringMixIn):
"""Represents a list of filenames as returned by notmuch
- This object contains the Filenames iterator. The main function is
- as_generator() which will return a generator so we can do a Filenamesth an
- iterator over a list of notmuch filenames. Do note that the underlying
- library only provides a one-time iterator (it cannot reset the iterator to
- the start). Thus iterating over the function will "exhaust" the list of
- tags, and a subsequent iteration attempt will raise a
- :exc:`NotInitializedError`. Also note, that any function that uses
- iteration (nearly all) will also exhaust the tags. So both::
+ Objects of this class implement the iterator protocol.
- for name in filenames: print name
+ .. note::
- as well as::
+ The underlying library only provides a one-time iterator (it
+ cannot reset the iterator to the start). Thus iterating over
+ the function will "exhaust" the list of tags, and a subsequent
+ iteration attempt will raise a
+ :exc:`NotInitializedError`. Also note, that any function that
+ uses iteration (nearly all) will also exhaust the tags. So
+ both::
- number_of_names = len(names)
+ for name in filenames: print name
- and even a simple::
+ as well as::
- #str() iterates over all tags to construct a space separated list
- print(str(filenames))
+ number_of_names = len(names)
- will "exhaust" the Filenames. However, you can use
- :meth:`Message.get_filenames` repeatedly to get fresh Filenames
- objects to perform various actions on filenames.
+ and even a simple::
+
+ #str() iterates over all tags to construct a space separated list
+ print(str(filenames))
+
+ will "exhaust" the Filenames. However, you can use
+ :meth:`Message.get_filenames` repeatedly to get fresh
+ Filenames objects to perform various actions on filenames.
"""
#notmuch_filenames_get
@@ -109,28 +112,13 @@ class Filenames(Python3StringMixIn):
return file_.decode('utf-8', 'ignore')
next = __next__ # python2.x iterator protocol compatibility
- def as_generator(self):
- """Return generator of Filenames
-
- This is the main function that will usually be used by the
- user.
-
- .. deprecated:: 0.12
- :class:`Filenames` objects implement the
- iterator protocol.
- """
- return self
-
def __unicode__(self):
"""Represent Filenames() as newline-separated list of full paths
- .. note:: As this iterates over the filenames, we will not be
- able to iterate over them again (as in retrieve them)! If
- the tags have been exhausted already, this will raise a
- :exc:`NotInitializedError` on subsequent
- attempts. However, you can use
- :meth:`Message.get_filenames` repeatedly to perform
- various actions on filenames.
+ .. note::
+
+ This method exhausts the iterator object, so you will not be able to
+ iterate over them again.
"""
return "\n".join(self)
@@ -140,7 +128,7 @@ class Filenames(Python3StringMixIn):
def __del__(self):
"""Close and free the notmuch filenames"""
- if self._files_p is not None:
+ if self._files_p:
self._destroy(self._files_p)
def __len__(self):
@@ -148,15 +136,8 @@ class Filenames(Python3StringMixIn):
.. note::
- As this iterates over the files, we will not be able to
- iterate over them again! So this will fail::
-
- #THIS FAILS
- files = Database().get_directory('').get_child_files()
- if len(files) > 0: # this 'exhausts' msgs
- # next line raises
- # NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
- for file in files: print file
+ This method exhausts the iterator object, so you will not be able to
+ iterate over them again.
"""
if not self._files_p:
raise NotInitializedError()
diff --git a/bindings/python/notmuch/globals.py b/bindings/python/notmuch/globals.py
index 442f3e35..f5fad72a 100644
--- a/bindings/python/notmuch/globals.py
+++ b/bindings/python/notmuch/globals.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
import sys
from ctypes import CDLL, Structure, POINTER
@@ -22,7 +22,7 @@ from ctypes import CDLL, Structure, POINTER
#-----------------------------------------------------------------------------
#package-global instance of the notmuch library
try:
- nmlib = CDLL("libnotmuch.so.2")
+ nmlib = CDLL("libnotmuch.so.3")
except:
raise ImportError("Could not find shared 'notmuch' library.")
diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py
index 9eb4feef..0e65694e 100644
--- a/bindings/python/notmuch/message.py
+++ b/bindings/python/notmuch/message.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
Jesse Rosenthal <jrosenthal@jhu.edu>
"""
@@ -259,7 +259,7 @@ class Message(Python3StringMixIn):
files_p = Message._get_filenames(self._msg)
- return Filenames(files_p, self).as_generator()
+ return Filenames(files_p, self)
def get_flag(self, flag):
"""Checks whether a specific flag is set for this message
@@ -614,7 +614,15 @@ class Message(Python3StringMixIn):
"""Create an internal representation of the message parts,
which can easily be output to json, text, or another output
format. The argument match tells whether this matched a
- query."""
+ query.
+
+ .. deprecated:: 0.13
+ This code adds functionality at the python
+ level that is unlikely to be useful for
+ anyone. Furthermore the python bindings strive
+ to be a thin wrapper around libnotmuch, so
+ this code will be removed in notmuch 0.14.
+ """
output = {}
output["id"] = self.get_message_id()
output["match"] = self.is_match()
@@ -660,12 +668,29 @@ class Message(Python3StringMixIn):
def format_message_as_json(self, indent=0):
"""Outputs the message as json. This is essentially the same
as python's dict format, but we run it through, just so we
- don't have to worry about the details."""
+ don't have to worry about the details.
+
+ .. deprecated:: 0.13
+ This code adds functionality at the python
+ level that is unlikely to be useful for
+ anyone. Furthermore the python bindings strive
+ to be a thin wrapper around libnotmuch, so
+ this code will be removed in notmuch 0.14.
+ """
return json.dumps(self.format_message_internal())
def format_message_as_text(self, indent=0):
"""Outputs it in the old-fashioned notmuch text form. Will be
- easy to change to a new format when the format changes."""
+ easy to change to a new format when the format changes.
+
+ .. deprecated:: 0.13
+ This code adds functionality at the python
+ level that is unlikely to be useful for
+ anyone. Furthermore the python bindings strive
+ to be a thin wrapper around libnotmuch, so
+ this code will be removed in notmuch 0.14.
+ """
+
format = self.format_message_internal()
output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
@@ -741,5 +766,5 @@ class Message(Python3StringMixIn):
def __del__(self):
"""Close and free the notmuch Message"""
- if self._msg is not None:
+ if self._msg:
self._destroy(self._msg)
diff --git a/bindings/python/notmuch/messages.py b/bindings/python/notmuch/messages.py
index d94f91b4..59ef40af 100644
--- a/bindings/python/notmuch/messages.py
+++ b/bindings/python/notmuch/messages.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
Jesse Rosenthal <jrosenthal@jhu.edu>
"""
@@ -172,11 +172,15 @@ class Messages(object):
next = __next__ # python2.x iterator protocol compatibility
def __nonzero__(self):
- """
- :return: True if there is at least one more thread in the
- Iterator, False if not."""
- return self._msgs is not None and \
- self._valid(self._msgs) > 0
+ '''
+ Implement truth value testing. If __nonzero__ is not
+ implemented, the python runtime would fall back to `len(..) >
+ 0` thus exhausting the iterator.
+
+ :returns: True if the wrapped iterator has at least one more object
+ left.
+ '''
+ return self._msgs and self._valid(self._msgs)
_destroy = nmlib.notmuch_messages_destroy
_destroy.argtypes = [NotmuchMessagesP]
@@ -184,7 +188,7 @@ class Messages(object):
def __del__(self):
"""Close and free the notmuch Messages"""
- if self._msgs is not None:
+ if self._msgs:
self._destroy(self._msgs)
def format_messages(self, format, indent=0, entire_thread=False):
diff --git a/bindings/python/notmuch/query.py b/bindings/python/notmuch/query.py
index ddaf8e08..756e63b5 100644
--- a/bindings/python/notmuch/query.py
+++ b/bindings/python/notmuch/query.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p, c_uint
@@ -203,5 +203,5 @@ class Query(object):
def __del__(self):
"""Close and free the Query"""
- if self._query is not None:
+ if self._query:
self._destroy(self._query)
diff --git a/bindings/python/notmuch/tag.py b/bindings/python/notmuch/tag.py
index 711bf533..363c3487 100644
--- a/bindings/python/notmuch/tag.py
+++ b/bindings/python/notmuch/tag.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p
from notmuch.globals import (
@@ -109,15 +109,15 @@ class Tags(Python3StringMixIn):
next = __next__ # python2.x iterator protocol compatibility
def __nonzero__(self):
- """Implement bool(Tags) check that can be repeatedly used
+ '''
+ Implement truth value testing. If __nonzero__ is not
+ implemented, the python runtime would fall back to `len(..) >
+ 0` thus exhausting the iterator.
- If __nonzero__ is not implemented, "if Tags()"
- will implicitly call __len__, using up our iterator, so it is
- important that this function is defined.
-
- :returns: True if the Tags() iterator has at least one more Tag
- left."""
- return self._valid(self._tags) > 0
+ :returns: True if the wrapped iterator has at least one more object
+ left.
+ '''
+ return self._tags and self._valid(self._tags)
def __unicode__(self):
"""string representation of :class:`Tags`: a space separated list of tags
@@ -137,5 +137,5 @@ class Tags(Python3StringMixIn):
def __del__(self):
"""Close and free the notmuch tags"""
- if self._tags is not None:
+ if self._tags:
self._destroy(self._tags)
diff --git a/bindings/python/notmuch/thread.py b/bindings/python/notmuch/thread.py
index a759c909..2f60d493 100644
--- a/bindings/python/notmuch/thread.py
+++ b/bindings/python/notmuch/thread.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p, c_long, c_int
@@ -260,5 +260,5 @@ class Thread(object):
def __del__(self):
"""Close and free the notmuch Thread"""
- if self._thread is not None:
+ if self._thread:
self._destroy(self._thread)
diff --git a/bindings/python/notmuch/threads.py b/bindings/python/notmuch/threads.py
index 225f5246..d2e0a910 100644
--- a/bindings/python/notmuch/threads.py
+++ b/bindings/python/notmuch/threads.py
@@ -14,7 +14,7 @@ for more details.
You should have received a copy of the GNU General Public License
along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from notmuch.globals import (
@@ -157,18 +157,15 @@ class Threads(Python3StringMixIn):
return i
def __nonzero__(self):
- """Check if :class:`Threads` contains at least one more valid thread
+ '''
+ Implement truth value testing. If __nonzero__ is not
+ implemented, the python runtime would fall back to `len(..) >
+ 0` thus exhausting the iterator.
- The existence of this function makes 'if Threads: foo' work, as
- that will implicitely call len() exhausting the iterator if
- __nonzero__ does not exist. This function makes `bool(Threads())`
- work repeatedly.
-
- :return: True if there is at least one more thread in the
- Iterator, False if not. None on a "Out-of-memory" error.
- """
- return self._threads is not None and \
- self._valid(self._threads) > 0
+ :returns: True if the wrapped iterator has at least one more object
+ left.
+ '''
+ return self._threads and self._valid(self._threads)
_destroy = nmlib.notmuch_threads_destroy
_destroy.argtypes = [NotmuchThreadsP]
@@ -176,5 +173,5 @@ class Threads(Python3StringMixIn):
def __del__(self):
"""Close and free the notmuch Threads"""
- if self._threads is not None:
+ if self._threads:
self._destroy(self._threads)
diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py
index 24e1d4c9..90bcadbe 100644
--- a/bindings/python/notmuch/version.py
+++ b/bindings/python/notmuch/version.py
@@ -1,2 +1,2 @@
# this file should be kept in sync with ../../../version
-__VERSION__ = '0.12'
+__VERSION__ = '0.13.2'
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
index 2e58dab1..f4c338e3 100644
--- a/bindings/python/setup.py
+++ b/bindings/python/setup.py
@@ -1,14 +1,33 @@
#!/usr/bin/env python
+"""
+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/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
import os
-import re
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__)),
+version_file = os.path.join(os.path.dirname(__file__),
'notmuch', 'version.py')
exec(compile(open(version_file).read(), version_file, 'exec'))
-assert __VERSION__, 'Failed to read the notmuch binding version number'
+assert '__VERSION__' in globals(), \
+ 'Failed to read the notmuch binding version number'
setup(name='notmuch',
version=__VERSION__,
@@ -16,32 +35,20 @@ setup(name='notmuch',
author='Sebastian Spaeth',
author_email='Sebastian@SSpaeth.de',
url='http://notmuchmail.org/',
- download_url='http://notmuchmail.org/releases/notmuch-'+__VERSION__+'.tar.gz',
+ download_url='http://notmuchmail.org/releases/notmuch-%s.tar.gz' % __VERSION__,
packages=['notmuch'],
- keywords = ["library", "email"],
- long_description="""Overview
-==============
-
-The notmuch module provides an interface to the `notmuch <http://notmuchmail.org>`_ functionality, directly interfacing with a shared notmuch library. Notmuch provides a maildatabase that allows for extremely quick searching and filtering of your email according to various criteria.
-
-The documentation for the latest cnotmuch release can be `viewed online <http://packages.python.org/notmuch>`_.
-
-The classes notmuch.Database, notmuch.Query provide most of the core functionality, returning notmuch.Messages and notmuch.Tags.
-
-Installation and Deinstallation
--------------------------------
-
-notmuch is included in the upstream notmuch source repository and it is
-packaged on http://pypi.python.org. This means you can do "easy_install
-notmuch" (or using pip) on your linux box and it will get installed
-into:
+ keywords=['library', 'email'],
+ long_description='''Overview
+========
-/usr/local/lib/python2.x/dist-packages/
+The notmuch module provides an interface to the `notmuch
+<http://notmuchmail.org>`_ functionality, directly interfacing with a
+shared notmuch library. Notmuch provides a maildatabase that allows
+for extremely quick searching and filtering of your email according to
+various criteria.
-For uninstalling, you will need to remove the "notmuch-0.x-py2.x.egg"
-directory and delete one entry refering to cnotmuch in the
-"easy-install.pth" file in that directory. There should be no trace
-left of cnotmuch then.
+The documentation for the latest notmuch release can be `viewed
+online <http://notmuch.readthedocs.org/>`_.
Requirements
------------
@@ -49,7 +56,7 @@ Requirements
You need to have notmuch installed (or rather libnotmuch.so.1). Also,
notmuch makes use of the ctypes library, and has only been tested with
python >= 2.5. It will not work on earlier python versions.
-""",
+''',
classifiers=['Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
diff --git a/bindings/ruby/database.c b/bindings/ruby/database.c
index 982fd592..e84f726d 100644
--- a/bindings/ruby/database.c
+++ b/bindings/ruby/database.c
@@ -42,6 +42,8 @@ notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE self)
int create, mode;
VALUE pathv, hashv;
VALUE modev;
+ notmuch_database_t *database;
+ notmuch_status_t ret;
/* Check arguments */
rb_scan_args (argc, argv, "11", &pathv, &hashv);
@@ -73,9 +75,13 @@ notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE self)
}
Check_Type (self, T_DATA);
- DATA_PTR (self) = create ? notmuch_database_create (path) : notmuch_database_open (path, mode);
- if (!DATA_PTR (self))
- rb_raise (notmuch_rb_eDatabaseError, "Failed to open database");
+ if (create)
+ ret = notmuch_database_create (path, &database);
+ else
+ ret = notmuch_database_open (path, mode, &database);
+ notmuch_rb_status_raise (ret);
+
+ DATA_PTR (self) = database;
return self;
}
@@ -110,7 +116,7 @@ notmuch_rb_database_close (VALUE self)
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
- notmuch_database_close (db);
+ notmuch_database_destroy (db);
DATA_PTR (self) = NULL;
return Qnil;
@@ -246,6 +252,7 @@ VALUE
notmuch_rb_database_get_directory (VALUE self, VALUE pathv)
{
const char *path;
+ notmuch_status_t ret;
notmuch_directory_t *dir;
notmuch_database_t *db;
@@ -254,11 +261,11 @@ notmuch_rb_database_get_directory (VALUE self, VALUE pathv)
SafeStringValue (pathv);
path = RSTRING_PTR (pathv);
- dir = notmuch_database_get_directory (db, path);
- if (!dir)
- rb_raise (notmuch_rb_eXapianError, "Xapian exception");
-
- return Data_Wrap_Struct (notmuch_rb_cDirectory, NULL, NULL, dir);
+ ret = notmuch_database_get_directory (db, path, &dir);
+ notmuch_rb_status_raise (ret);
+ if (dir)
+ return Data_Wrap_Struct (notmuch_rb_cDirectory, NULL, NULL, dir);
+ return Qnil;
}
/*
diff --git a/bindings/ruby/defs.h b/bindings/ruby/defs.h
index 81f652fb..3f9512ba 100644
--- a/bindings/ruby/defs.h
+++ b/bindings/ruby/defs.h
@@ -1,6 +1,6 @@
/* The Ruby interface to the notmuch mail library
*
- * Copyright © 2010, 2011 Ali Polatel
+ * Copyright © 2010, 2011, 2012 Ali Polatel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -217,11 +217,20 @@ VALUE
notmuch_rb_query_get_string (VALUE self);
VALUE
+notmuch_rb_query_add_tag_exclude (VALUE self, VALUE tagv);
+
+VALUE
+notmuch_rb_query_set_omit_excluded (VALUE self, VALUE omitv);
+
+VALUE
notmuch_rb_query_search_threads (VALUE self);
VALUE
notmuch_rb_query_search_messages (VALUE self);
+VALUE
+notmuch_rb_query_count_messages (VALUE self);
+
/* threads.c */
VALUE
notmuch_rb_threads_destroy (VALUE self);
diff --git a/bindings/ruby/extconf.rb b/bindings/ruby/extconf.rb
index ccac609c..7b9750f2 100644
--- a/bindings/ruby/extconf.rb
+++ b/bindings/ruby/extconf.rb
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
# coding: utf-8
-# Copyright 2010, 2011 Ali Polatel <alip@exherbo.org>
+# Copyright 2010, 2011, 2012 Ali Polatel <alip@exherbo.org>
# Distributed under the terms of the GNU General Public License v3
require 'mkmf'
diff --git a/bindings/ruby/init.c b/bindings/ruby/init.c
index 4405f196..3fe60fb7 100644
--- a/bindings/ruby/init.c
+++ b/bindings/ruby/init.c
@@ -1,6 +1,6 @@
/* The Ruby interface to the notmuch mail library
*
- * Copyright © 2010, 2011 Ali Polatel
+ * Copyright © 2010, 2011, 2012 Ali Polatel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -96,6 +96,12 @@ Init_notmuch (void)
*/
rb_define_const (mod, "MESSAGE_FLAG_MATCH", INT2FIX (NOTMUCH_MESSAGE_FLAG_MATCH));
/*
+ * Document-const: Notmuch::MESSAGE_FLAG_EXCLUDED
+ *
+ * Message flag "excluded"
+ */
+ rb_define_const (mod, "MESSAGE_FLAG_EXCLUDED", INT2FIX (NOTMUCH_MESSAGE_FLAG_EXCLUDED));
+ /*
* Document-const: Notmuch::TAG_MAX
*
* Maximum allowed length of a tag
@@ -234,8 +240,11 @@ Init_notmuch (void)
rb_define_method (notmuch_rb_cQuery, "sort", notmuch_rb_query_get_sort, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "sort=", notmuch_rb_query_set_sort, 1); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "to_s", notmuch_rb_query_get_string, 0); /* in query.c */
+ rb_define_method (notmuch_rb_cQuery, "add_tag_exclude", notmuch_rb_query_add_tag_exclude, 1); /* in query.c */
+ rb_define_method (notmuch_rb_cQuery, "omit_excluded=", notmuch_rb_query_set_omit_excluded, 1); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "search_threads", notmuch_rb_query_search_threads, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "search_messages", notmuch_rb_query_search_messages, 0); /* in query.c */
+ rb_define_method (notmuch_rb_cQuery, "count_messages", notmuch_rb_query_count_messages, 0); /* in query.c */
/*
* Document-class: Notmuch::Threads
diff --git a/bindings/ruby/query.c b/bindings/ruby/query.c
index 74fd5858..e5ba1b7a 100644
--- a/bindings/ruby/query.c
+++ b/bindings/ruby/query.c
@@ -1,6 +1,6 @@
/* The Ruby interface to the notmuch mail library
*
- * Copyright © 2010, 2011 Ali Polatel
+ * Copyright © 2010, 2011, 2012 Ali Polatel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -89,6 +89,42 @@ notmuch_rb_query_get_string (VALUE self)
}
/*
+ * call-seq: QUERY.add_tag_exclude(tag) => nil
+ *
+ * Add a tag that will be excluded from the query results by default.
+ */
+VALUE
+notmuch_rb_query_add_tag_exclude (VALUE self, VALUE tagv)
+{
+ notmuch_query_t *query;
+ const char *tag;
+
+ Data_Get_Notmuch_Query (self, query);
+ tag = RSTRING_PTR(tagv);
+
+ notmuch_query_add_tag_exclude(query, tag);
+ return Qnil;
+}
+
+/*
+ * call-seq: QUERY.omit_excluded=(boolean) => nil
+ *
+ * Specify whether to omit excluded results or simply flag them.
+ * By default, this is set to +true+.
+ */
+VALUE
+notmuch_rb_query_set_omit_excluded (VALUE self, VALUE omitv)
+{
+ notmuch_query_t *query;
+
+ Data_Get_Notmuch_Query (self, query);
+
+ notmuch_query_set_omit_excluded (query, RTEST (omitv));
+
+ return Qnil;
+}
+
+/*
* call-seq: QUERY.search_threads => THREADS
*
* Search for threads
@@ -127,3 +163,22 @@ notmuch_rb_query_search_messages (VALUE self)
return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages);
}
+
+/*
+ * call-seq: QUERY.count_messages => Fixnum
+ *
+ * Return an estimate of the number of messages matching a search
+ */
+VALUE
+notmuch_rb_query_count_messages (VALUE self)
+{
+ notmuch_query_t *query;
+
+ Data_Get_Notmuch_Query (self, query);
+
+ /* Xapian exceptions are not handled properly.
+ * (function may return 0 after printing a message)
+ * Thus there is nothing we can do here...
+ */
+ return UINT2FIX(notmuch_query_count_messages(query));
+}
diff --git a/command-line-arguments.c b/command-line-arguments.c
index e7114143..76b185f8 100644
--- a/command-line-arguments.c
+++ b/command-line-arguments.c
@@ -28,6 +28,24 @@ _process_keyword_arg (const notmuch_opt_desc_t *arg_desc, const char *arg_str) {
return FALSE;
}
+static notmuch_bool_t
+_process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
+
+ if (next == 0) {
+ *((notmuch_bool_t *)arg_desc->output_var) = TRUE;
+ return TRUE;
+ }
+ if (strcmp (arg_str, "false") == 0) {
+ *((notmuch_bool_t *)arg_desc->output_var) = FALSE;
+ return TRUE;
+ }
+ if (strcmp (arg_str, "true") == 0) {
+ *((notmuch_bool_t *)arg_desc->output_var) = TRUE;
+ return TRUE;
+ }
+ return FALSE;
+}
+
/*
Search for the {pos_arg_index}th position argument, return FALSE if
that does not exist.
@@ -76,14 +94,15 @@ parse_option (const char *arg,
char *endptr;
/* Everything but boolean arguments (switches) needs a
- * delimiter, and a non-zero length value
+ * delimiter, and a non-zero length value. Boolean
+ * arguments may take an optional =true or =false value.
*/
-
- if (try->opt_type != NOTMUCH_OPT_BOOLEAN) {
- if (next != '=' && next != ':') return FALSE;
- if (value[0] == 0) return FALSE;
+ if (next != '=' && next != ':' && next != 0) return FALSE;
+ if (next == 0) {
+ if (try->opt_type != NOTMUCH_OPT_BOOLEAN)
+ return FALSE;
} else {
- if (next != 0) return FALSE;
+ if (value[0] == 0) return FALSE;
}
if (try->output_var == NULL)
@@ -94,8 +113,7 @@ parse_option (const char *arg,
return _process_keyword_arg (try, value);
break;
case NOTMUCH_OPT_BOOLEAN:
- *((notmuch_bool_t *)try->output_var) = TRUE;
- return TRUE;
+ return _process_boolean_arg (try, next, value);
break;
case NOTMUCH_OPT_INT:
*((int *)try->output_var) = strtol (value, &endptr, 10);
diff --git a/configure b/configure
index ee0ae737..71981b7c 100755
--- a/configure
+++ b/configure
@@ -1,5 +1,8 @@
#! /bin/sh
+# Store original IFS value so it can be changed (and restored) in many places.
+readonly DEFAULT_IFS=$IFS
+
srcdir=$(dirname "$0")
# For a non-srcdir configure invocation (such as ../configure), create
@@ -45,6 +48,16 @@ WITH_EMACS=1
WITH_BASH=1
WITH_ZSH=1
+# Compatible GMime versions (with constraints).
+# If using GMime 2.6, we need to have a version >= 2.6.5 to avoid a
+# crypto bug. We need 2.6.7 for permissive "From " header handling.
+GMIME_24_VERSION_CTR=''
+GMIME_24_VERSION="gmime-2.4 $GMIME_24_VERSION_CTR"
+GMIME_26_VERSION_CTR='>= 2.6.7'
+GMIME_26_VERSION="gmime-2.6 $GMIME_26_VERSION_CTR"
+
+WITH_GMIME_VERSIONS="$GMIME_26_VERSION;$GMIME_24_VERSION"
+
usage ()
{
cat <<EOF
@@ -170,38 +183,16 @@ for option; do
fi
elif [ "${option}" = '--without-zsh-completion' ] ; then
WITH_ZSH=0
+ elif [ "${option%%=*}" = '--with-gmime-version' ] ; then
+ if [ "${option#*=}" = '2.4' ]; then
+ WITH_GMIME_VERSIONS=$GMIME_24_VERSION
+ elif [ "${option#*=}" = '2.6' ]; then
+ WITH_GMIME_VERSIONS=$GMIME_26_VERSION
+ fi
elif [ "${option%%=*}" = '--build' ] ; then
- build_option="${option#*=}"
- case ${build_option} in
- *-*-*) ;;
- *)
- echo "Unrecognized value for --build option: ${build_option}"
- echo "Should be: <cpu>-<vendor>-<os>"
- echo "See:"
- echo " $0 --help"
- echo ""
- exit 1
- esac
- build_cpu=${build_option%%-*}
- build_option=${build_option#*-}
- build_vendor=${build_option%%-*}
- build_os=${build_option#*-}
+ true
elif [ "${option%%=*}" = '--host' ] ; then
- host_option="${option#*=}"
- case ${host_option} in
- *-*-*) ;;
- *)
- echo "Unrecognized value for --host option: ${host_option}"
- echo "Should be: <cpu>-<vendor>-<os>"
- echo "See:"
- echo " $0 --help"
- echo ""
- exit 1
- esac
- host_cpu=${host_option%%-*}
- host_option=${host_option#*-}
- host_vendor=${host_option%%-*}
- host_os=${host_option#*-}
+ true
elif [ "${option%%=*}" = '--infodir' ] ; then
true
elif [ "${option%%=*}" = '--datadir' ] ; then
@@ -273,11 +264,10 @@ if [ ${have_xapian} = "0" ]; then
errors=$((errors + 1))
fi
-# If using GMime 2.6, we need to have a version >= 2.6.5 to avoid a
-# crypto bug. We need 2.6.7 for permissive "From " header handling.
printf "Checking for GMime development files... "
have_gmime=0
-for gmimepc in 'gmime-2.6 >= 2.6.7' gmime-2.4; do
+IFS=';'
+for gmimepc in $WITH_GMIME_VERSIONS; do
if pkg-config --exists $gmimepc; then
printf "Yes ($gmimepc).\n"
have_gmime=1
@@ -286,6 +276,7 @@ for gmimepc in 'gmime-2.6 >= 2.6.7' gmime-2.4; do
break
fi
done
+IFS=$DEFAULT_IFS
if [ "$have_gmime" = "0" ]; then
printf "No.\n"
errors=$((errors + 1))
@@ -365,9 +356,9 @@ elif [ $uname = "SunOS" ] ; then
printf "Solaris.\n"
platform=SOLARIS
linker_resolves_library_dependencies=0
-elif [ $uname = "Linux" ] ; then
- printf "Linux\n"
- platform=LINUX
+elif [ $uname = "Linux" ] || [ $uname = "GNU" ] ; then
+ printf "$uname\n"
+ platform="$uname"
linker_resolves_library_dependencies=1
printf "Checking for $libdir_expanded in ldconfig... "
@@ -379,7 +370,6 @@ elif [ $uname = "Linux" ] ; then
# IFS=$(printf '\n')
#
# because the shell's command substitution deletes any trailing newlines.
- OLD_IFS=$IFS
IFS="
"
for path in $ldconfig_paths; do
@@ -387,7 +377,7 @@ elif [ $uname = "Linux" ] ; then
libdir_in_ldconfig=1
fi
done
- IFS=$OLD_IFS
+ IFS=$DEFAULT_IFS
if [ "$libdir_in_ldconfig" = '0' ]; then
printf "No (will set RPATH)\n"
else
@@ -415,26 +405,29 @@ EOF
echo " http://xapian.org/"
fi
if [ $have_gmime -eq 0 ]; then
- echo " GMime 2.4 library (including development files such as headers)"
+ echo " Either GMime 2.4 library" $GMIME_24_VERSION_CTR "or GMime 2.6 library" $GMIME_26_VERSION_CTR
+ echo " (including development files such as headers)"
echo " http://spruce.sourceforge.net/gmime/"
+ echo
fi
if [ $have_glib -eq 0 ]; then
echo " Glib library >= 2.22 (including development files such as headers)"
echo " http://ftp.gnome.org/pub/gnome/sources/glib/"
+ echo
fi
if [ $have_talloc -eq 0 ]; then
echo " The talloc library (including development files such as headers)"
echo " http://talloc.samba.org/"
+ echo
fi
cat <<EOF
-
With any luck, you're using a modern, package-based operating system
that has all of these packages available in the distribution. In that
case a simple command will install everything you need. For example:
On Debian and similar systems:
- sudo apt-get install libxapian-dev libgmime-2.4-dev libtalloc-dev
+ sudo apt-get install libxapian-dev libgmime-2.6-dev libtalloc-dev
Or on Fedora and similar systems:
@@ -536,7 +529,7 @@ done
printf "\n\t${WARN_CFLAGS}\n"
rm -f minimal minimal.c
-
+
cat <<EOF
All required packages were found. You may now run the following
diff --git a/contrib/notmuch-deliver/src/main.c b/contrib/notmuch-deliver/src/main.c
index 6f32f73d..032b9d62 100644
--- a/contrib/notmuch-deliver/src/main.c
+++ b/contrib/notmuch-deliver/src/main.c
@@ -359,6 +359,7 @@ main(int argc, char **argv)
GOptionContext *ctx;
GError *error = NULL;
notmuch_database_t *db;
+ notmuch_status_t status;
ctx = g_option_context_new("[FOLDER]");
g_option_context_add_main_entries(ctx, options, PACKAGE);
@@ -429,7 +430,14 @@ main(int argc, char **argv)
maildir = g_strdup(db_path);
g_debug("Opening notmuch database `%s'", db_path);
- db = notmuch_database_open(db_path, NOTMUCH_DATABASE_MODE_READ_WRITE);
+ status = notmuch_database_open(db_path, NOTMUCH_DATABASE_MODE_READ_WRITE,
+ &db);
+ if (status) {
+ g_critical("Failed to open database `%s': %s",
+ db_path, notmuch_status_to_string(status));
+ g_free(maildir);
+ return EX_SOFTWARE;
+ }
g_free(db_path);
if (db == NULL)
return EX_SOFTWARE;
@@ -455,7 +463,7 @@ main(int argc, char **argv)
g_strfreev(opt_rtags);
g_free(mail);
- notmuch_database_close(db);
+ notmuch_database_destroy(db);
return 0;
}
diff --git a/contrib/notmuch-mutt/.gitignore b/contrib/notmuch-mutt/.gitignore
new file mode 100644
index 00000000..682a5779
--- /dev/null
+++ b/contrib/notmuch-mutt/.gitignore
@@ -0,0 +1,2 @@
+notmuch-mutt.1
+README.html
diff --git a/contrib/notmuch-mutt/Makefile b/contrib/notmuch-mutt/Makefile
new file mode 100644
index 00000000..87f9031c
--- /dev/null
+++ b/contrib/notmuch-mutt/Makefile
@@ -0,0 +1,12 @@
+NAME = notmuch-mutt
+
+all: $(NAME) $(NAME).1
+
+$(NAME).1: $(NAME)
+ pod2man $< > $@
+
+README.html: README
+ markdown $< > $@
+
+clean:
+ rm -f notmuch-mutt.1 README.html
diff --git a/contrib/notmuch-mutt/README b/contrib/notmuch-mutt/README
new file mode 100644
index 00000000..382ac911
--- /dev/null
+++ b/contrib/notmuch-mutt/README
@@ -0,0 +1,59 @@
+notmuch-mutt: Notmuch (of a) helper for Mutt
+============================================
+
+notmuch-mutt provide integration among the [Mutt] [1] mail user agent and the
+[Notmuch] [2] mail indexer.
+
+notmuch-mutt offer two main integration features. The first one is the ability
+of stating a **search query interactively** and then jump to a fresh Maildir
+containing its search results only. The second one is the ability to
+**reconstruct threads on the fly** starting from the currently highlighted
+mail, which comes handy when a thread has been split across different maildirs,
+archived, or the like.
+
+notmuch-mutt enables to trigger mail searches via a Mutt macro (usually F8) and
+reconstruct threads via another (usually F9). Check the manpage for the 2-liner
+configuration snippet for your Mutt configuration files (~/.muttrc,
+/etc/Muttrc, or a /etc/Muttrc.d snippet).
+
+A [blog style introduction] [3] to notmuch-mutt is available and includes some
+more rationale for its existence.
+
+Arguably, some of the logics of notmuch-mutt could disappear by adding support
+for a --output=symlinks flag to notmuch.
+
+
+[1]: http://www.mutt.org/
+[2]: http://notmuchmail.org/
+[3]: http://upsilon.cc/~zack/blog/posts/2011/01/how_to_use_Notmuch_with_Mutt/
+
+
+Requirements
+------------
+
+To *run* notmuch-mutt you will need Perl with the following libraries:
+
+- Mail::Box <http://search.cpan.org/~markov/Mail-Box/>
+ (Debian package: libmail-box-perl)
+- Mail::Internet <http://search.cpan.org/~markov/MailTools/>
+ (Debian package: libmailtools-perl)
+- String::ShellQuote <http://search.cpan.org/~rosch/String-ShellQuote/ShellQuote.pm>
+ (Debian package: libstring-shellquote-perl)
+- Term::ReadLine <http://search.cpan.org/~hayashi/Term-ReadLine-Gnu/>
+ (Debian package: libterm-readline-gnu-perl)
+
+To *build* notmuch-mutt documentation you will need:
+
+- pod2man (coming with Perl) to generate the manpage
+- markdown to generate README.html out of this file
+
+
+License
+-------
+
+notmuch-mutt is copyright (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>.
+
+notmuch-mutt is released under the terms of the GNU General Public License
+(GPL), version 3 or above. A copy of the license is available online at
+<http://www.gnu.org/licenses/>.
+
diff --git a/contrib/notmuch-mutt/notmuch-mutt b/contrib/notmuch-mutt/notmuch-mutt
new file mode 100755
index 00000000..71206c35
--- /dev/null
+++ b/contrib/notmuch-mutt/notmuch-mutt
@@ -0,0 +1,238 @@
+#!/usr/bin/perl -w
+#
+# notmuch-mutt - notmuch (of a) helper for Mutt
+#
+# Copyright: 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# See the bottom of this file for more documentation.
+# A manpage can be obtained by running "pod2man notmuch-mutt > notmuch-mutt.1"
+
+use strict;
+use warnings;
+
+use File::Path;
+use Getopt::Long qw(:config no_getopt_compat);
+use Mail::Internet;
+use Mail::Box::Maildir;
+use Pod::Usage;
+use String::ShellQuote;
+use Term::ReadLine;
+
+
+my $xdg_cache_dir = "$ENV{HOME}/.cache";
+$xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME};
+my $cache_dir = "$xdg_cache_dir/notmuch/mutt";
+
+
+# create an empty maildir (if missing) or empty an existing maildir"
+sub empty_maildir($) {
+ my ($maildir) = (@_);
+ rmtree($maildir) if (-d $maildir);
+ my $folder = new Mail::Box::Maildir(folder => $maildir,
+ create => 1);
+ $folder->close();
+}
+
+# search($maildir, $query)
+# search mails according to $query with notmuch; store results in $maildir
+sub search($$) {
+ my ($maildir, $query) = @_;
+ $query = shell_quote($query);
+
+ empty_maildir($maildir);
+ system("notmuch search --output=files $query"
+ . " | sed -e 's: :\\\\ :g'"
+ . " | xargs --no-run-if-empty ln -s -t $maildir/cur/");
+}
+
+sub prompt($$) {
+ my ($text, $default) = @_;
+ my $query = "";
+ my $term = Term::ReadLine->new( "notmuch-mutt" );
+ my $histfile = "$cache_dir/history";
+
+ $term->ornaments( 0 );
+ $term->unbind_key( ord( "\t" ) );
+ $term->MinLine( 3 );
+ $histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE};
+ $term->ReadHistory($histfile) if (-r $histfile);
+ while (1) {
+ chomp($query = $term->readline($text, $default));
+ if ($query eq "?") {
+ system("man", "notmuch");
+ } else {
+ $term->WriteHistory($histfile);
+ return $query;
+ }
+ }
+}
+
+sub get_message_id() {
+ my $mail = Mail::Internet->new(\*STDIN);
+ $mail->head->get("message-id") =~ /^<(.*)>$/; # get message-id
+ return $1;
+}
+
+sub search_action($$@) {
+ my ($interactive, $results_dir, @params) = @_;
+
+ if (! $interactive) {
+ search($results_dir, join(' ', @params));
+ } else {
+ my $query = prompt("search ('?' for man): ", join(' ', @params));
+ if ($query ne "") {
+ search($results_dir,$query);
+ }
+ }
+}
+
+sub thread_action(@) {
+ my ($results_dir, @params) = @_;
+
+ my $mid = get_message_id();
+ my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
+ my $tid = `$search_cmd`; # get thread id
+ chomp($tid);
+
+ search($results_dir, $tid);
+}
+
+sub tag_action(@) {
+ my $mid = get_message_id();
+
+ system("notmuch tag "
+ . shell_quote(join(' ', @_))
+ . " id:$mid");
+}
+
+sub die_usage() {
+ my %podflags = ( "verbose" => 1,
+ "exitval" => 2 );
+ pod2usage(%podflags);
+}
+
+sub main() {
+ mkpath($cache_dir) unless (-d $cache_dir);
+
+ my $results_dir = "$cache_dir/results";
+ my $interactive = 0;
+ my $help_needed = 0;
+
+ my $getopt = GetOptions(
+ "h|help" => \$help_needed,
+ "o|output-dir=s" => \$results_dir,
+ "p|prompt" => \$interactive);
+ if (! $getopt || $#ARGV < 0) { die_usage() };
+ my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
+
+ foreach my $param (@params) {
+ $param =~ s/folder:=/folder:/g;
+ }
+
+ if ($help_needed) {
+ die_usage();
+ } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
+ print STDERR "Error: no search term provided\n\n";
+ die_usage();
+ } elsif ($action eq "search") {
+ search_action($interactive, $results_dir, @params);
+ } elsif ($action eq "thread") {
+ thread_action($results_dir, @params);
+ } elsif ($action eq "tag") {
+ tag_action(@params);
+ } else {
+ die_usage();
+ }
+}
+
+main();
+
+__END__
+
+=head1 NAME
+
+notmuch-mutt - notmuch (of a) helper for Mutt
+
+=head1 SYNOPSIS
+
+=over
+
+=item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
+
+=item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
+
+=item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
+
+=back
+
+=head1 DESCRIPTION
+
+notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
+a maildir with search results.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -o DIR
+
+=item --output-dir DIR
+
+Store search results as (symlink) messages under maildir DIR. Beware: DIR will
+be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
+
+=item -p
+
+=item --prompt
+
+Instead of using command line search terms, prompt the user for them (only for
+"search").
+
+=item -h
+
+=item --help
+
+Show usage information and exit.
+
+=back
+
+=head1 INTEGRATION WITH MUTT
+
+notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
+(unsurprisingly, given the name). To that end, you should define macros like
+the following in your Mutt configuration (usually one of: F<~/.muttrc>,
+F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
+
+ macro index <F8> \
+ "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt --prompt search<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter>" \
+ "notmuch: search mail"
+ macro index <F9> \
+ "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt thread<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" \
+ "notmuch: reconstruct thread"
+ macro index <F6> \
+ "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -inbox<enter>" \
+ "notmuch: remove message from inbox"
+
+The first macro (activated by <F8>) prompts the user for notmuch search terms
+and then jump to a temporary maildir showing search results. The second macro
+(activated by <F9>) reconstructs the thread corresponding to the current mail
+and show it as search results. The third macro (activated by <F6>) removes the
+tag C<inbox> from the current message; by changing C<-inbox> this macro may be
+customised to add or remove tags appropriate to the users notmuch work-flow.
+
+To keep notmuch index current you should then periodically run C<notmuch
+new>. Depending on your local mail setup, you might want to do that via cron,
+as a hook triggered by mail retrieval, etc.
+
+=head1 SEE ALSO
+
+mutt(1), notmuch(1)
+
+=head1 AUTHOR
+
+Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
+
+License: GNU General Public License (GPL), version 3 or higher
+
+=cut
diff --git a/contrib/notmuch-mutt/notmuch-mutt.rc b/contrib/notmuch-mutt/notmuch-mutt.rc
new file mode 100644
index 00000000..c0ff000b
--- /dev/null
+++ b/contrib/notmuch-mutt/notmuch-mutt.rc
@@ -0,0 +1,9 @@
+macro index <F8> \
+ "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt --prompt search<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>" \
+ "notmuch: search mail"
+macro index <F9> \
+ "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt thread<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter><enter-command>set wait_key<enter>" \
+ "notmuch: reconstruct thread"
+macro index <F6> \
+ "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -inbox<enter>" \
+ "notmuch: remove message from inbox"
diff --git a/debian/changelog b/debian/changelog
index 11bdefb1..3434ea98 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,37 @@
+notmuch (0.13.2-1) unstable; urgency=low
+
+ * Upstream bugfix release. No changes to binary packages.
+
+ -- David Bremner <bremner@debian.org> Sat, 02 Jun 2012 18:16:01 -0300
+
+notmuch (0.13.1-1) unstable; urgency=low
+
+ * Upstream bugfix release.
+ - fix for encoding problems with reply in emacs
+ - notmuch_database_(get_directory|find_message_by_filename) now
+ work for read-only databases.
+
+ -- David Bremner <bremner@debian.org> Fri, 25 May 2012 21:19:06 -0300
+
+notmuch (0.13-1) unstable; urgency=low
+
+ * New upstream release. See /usr/share/doc/notmuch/NEWS.gz for changes.
+
+ [ Stefano Zacchiroli ]
+ * Recommend all notmuch UI (including notmuch-mutt) as alternatives,
+ to avoid unneeded vim/emacs installation. Thanks Matteo F. Vescovi
+ for the patch. (Closes: #673011)
+
+ -- David Bremner <bremner@debian.org> Tue, 15 May 2012 18:19:32 -0300
+
+notmuch (0.13~rc1-2) experimental; urgency=low
+
+ * New upstream pre-release
+ * new binary package "notmuch-mutt" for Mutt integration
+ * Bump libnotmuch SONAME because of API changes
+
+ -- David Bremner <bremner@debian.org> Sat, 05 May 2012 10:26:47 -0300
+
notmuch (0.12-1~bpo60+1) squeeze-backports; urgency=low
* Rebuild for squeeze-backports.
diff --git a/debian/control b/debian/control
index ed27c587..812430fb 100644
--- a/debian/control
+++ b/debian/control
@@ -25,8 +25,8 @@ Dm-Upload-Allowed: yes
Package: notmuch
Architecture: any
-Depends: libnotmuch2 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends}
-Recommends: notmuch-emacs | notmuch-vim
+Depends: libnotmuch3 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends}
+Recommends: notmuch-emacs | notmuch-vim | notmuch-mutt, gnupg-agent
Description: thread-based email index, search and tagging
Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses
@@ -35,7 +35,7 @@ Description: thread-based email index, search and tagging
.
This package contains the notmuch command-line interface
-Package: libnotmuch2
+Package: libnotmuch3
Section: libs
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
@@ -51,7 +51,7 @@ Description: thread-based email index, search and tagging (runtime)
Package: libnotmuch-dev
Section: libdevel
Architecture: any
-Depends: ${misc:Depends}, libnotmuch2 (= ${binary:Version})
+Depends: ${misc:Depends}, libnotmuch3 (= ${binary:Version})
Description: thread-based email index, search and tagging (development)
Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses
@@ -64,7 +64,7 @@ Description: thread-based email index, search and tagging (development)
Package: python-notmuch
Architecture: all
Section: python
-Depends: ${misc:Depends}, ${python:Depends}, libnotmuch2
+Depends: ${misc:Depends}, ${python:Depends}, libnotmuch3
Description: python interface to the notmuch mail search and index library
Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses
@@ -105,3 +105,19 @@ Description: thread-based email index, search and tagging (vim interface)
This package provides a vim based mail user agent based on
notmuch.
+Package: notmuch-mutt
+Architecture: all
+Depends: notmuch, libmail-box-perl, libmailtools-perl,
+ libstring-shellquote-perl, libterm-readline-gnu-perl,
+ ${misc:Depends}
+Recommends: mutt
+Enhances: notmuch, mutt
+Description: thread-based email index, search and tagging (Mutt interface)
+ notmuch-mutt provides integration among the Mutt mail user agent and
+ the Notmuch mail indexer.
+ .
+ notmuch-mutt offer two main integration features. The first one is
+ the ability of stating a search query interactively and then jump to
+ a fresh Maildir containing its search results only. The second one is
+ the ability to reconstruct threads on the fly starting from the
+ current highlighted mail.
diff --git a/debian/libnotmuch2.install b/debian/libnotmuch3.install
index da4fc25b..da4fc25b 100644
--- a/debian/libnotmuch2.install
+++ b/debian/libnotmuch3.install
diff --git a/debian/libnotmuch2.symbols b/debian/libnotmuch3.symbols
index 272467a3..140cae93 100644
--- a/debian/libnotmuch2.symbols
+++ b/debian/libnotmuch3.symbols
@@ -1,8 +1,9 @@
-libnotmuch.so.2 libnotmuch2 #MINVER#
+libnotmuch.so.3 libnotmuch3 #MINVER#
notmuch_database_add_message@Base 0.3
notmuch_database_begin_atomic@Base 0.9~rc1
- notmuch_database_close@Base 0.3
+ notmuch_database_close@Base 0.13~rc1
notmuch_database_create@Base 0.3
+ notmuch_database_destroy@Base 0.13~rc1
notmuch_database_end_atomic@Base 0.9~rc1
notmuch_database_find_message@Base 0.9~rc2
notmuch_database_find_message_by_filename@Base 0.9~rc2
@@ -55,6 +56,7 @@ libnotmuch.so.2 libnotmuch2 #MINVER#
notmuch_query_get_sort@Base 0.4
notmuch_query_search_messages@Base 0.3
notmuch_query_search_threads@Base 0.3
+ notmuch_query_set_omit_excluded@Base 0.13~rc1
notmuch_query_set_sort@Base 0.3
notmuch_status_to_string@Base 0.3
notmuch_tags_destroy@Base 0.3
diff --git a/debian/notmuch-mutt.docs b/debian/notmuch-mutt.docs
new file mode 100644
index 00000000..f3d25cdc
--- /dev/null
+++ b/debian/notmuch-mutt.docs
@@ -0,0 +1 @@
+contrib/notmuch-mutt/README
diff --git a/debian/notmuch-mutt.install b/debian/notmuch-mutt.install
new file mode 100644
index 00000000..9141c26f
--- /dev/null
+++ b/debian/notmuch-mutt.install
@@ -0,0 +1,2 @@
+contrib/notmuch-mutt/notmuch-mutt usr/bin
+contrib/notmuch-mutt/notmuch-mutt.rc etc/Muttrc.d
diff --git a/debian/notmuch-mutt.manpages b/debian/notmuch-mutt.manpages
new file mode 100644
index 00000000..3f6b8abd
--- /dev/null
+++ b/debian/notmuch-mutt.manpages
@@ -0,0 +1 @@
+contrib/notmuch-mutt/notmuch-mutt.1
diff --git a/debian/rules b/debian/rules
index 956f3f2c..603b3ab2 100755
--- a/debian/rules
+++ b/debian/rules
@@ -9,10 +9,12 @@ override_dh_auto_configure:
override_dh_auto_build:
dh_auto_build
dh_auto_build --sourcedirectory bindings/python
+ $(MAKE) -C contrib/notmuch-mutt
override_dh_auto_clean:
dh_auto_clean
dh_auto_clean --sourcedirectory bindings/python
+ $(MAKE) -C contrib/notmuch-mutt clean
override_dh_auto_install:
dh_auto_install
diff --git a/devel/TODO b/devel/TODO
index 4dda6f46..7b750afa 100644
--- a/devel/TODO
+++ b/devel/TODO
@@ -141,6 +141,14 @@ 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).
+Properly handle replying to multiple messages. Currently, the JSON
+reply format only supports a single message, but the default reply
+format accepts searches returning multiple messages. The expected
+behavior of replying to multiple messages is not obvious, and there
+are multiple ideas that might make sense. Some consensus needs to be
+reached on this issue, and then both reply formats should be updated
+to be consistent.
+
notmuch library
---------------
Add support for custom flag<->tag mappings. In the notmuch
diff --git a/devel/schemata b/devel/schemata
index 24ad7757..977cea75 100644
--- a/devel/schemata
+++ b/devel/schemata
@@ -36,7 +36,7 @@ thread_node = [
[thread_node*] # children of message
]
-# A message (show_message)
+# A message (format_part_json)
message = {
# (format_message_json)
id: messageid,
@@ -50,18 +50,13 @@ message = {
body: [part]
}
-# A MIME part (show_message_body)
+# A MIME part (format_part_json)
part = {
- # format_part_start_json
id: int|string, # part id (currently DFS part number)
- # format_part_encstatus_json
encstatus?: encstatus,
-
- # format_part_sigstatus_json
sigstatus?: sigstatus,
- # format_part_content_json
content-type: string,
content-id?: string,
# if content-type starts with "multipart/":
@@ -77,8 +72,7 @@ part = {
content?: string
}
-# The headers of a message (format_headers_json with raw headers) or
-# a part (format_headers_message_part_json with pretty-printed headers)
+# The headers of a message or part (format_headers_json with reply = FALSE)
headers = {
Subject: string,
From: string,
@@ -88,14 +82,14 @@ headers = {
Date: string
}
-# Encryption status (format_part_encstatus_json)
+# Encryption status (format_part_json)
encstatus = [{status: "good"|"bad"}]
# Signature status (format_part_sigstatus_json)
sigstatus = [signature*]
signature = {
- # signature_status_to_string
+ # (signature_status_to_string)
status: "none"|"good"|"bad"|"error"|"unknown",
# if status is "good":
fingerprint?: string,
@@ -136,3 +130,25 @@ thread = {
# matched and unmatched
subject: string
}
+
+notmuch reply schema
+--------------------
+
+reply = {
+ # The headers of the constructed reply
+ reply-headers: reply_headers,
+
+ # As in the show format (format_part_json)
+ original: message
+}
+
+# Reply headers (format_headers_json with reply = TRUE)
+reply_headers = {
+ Subject: string,
+ From: string,
+ To?: string,
+ Cc?: string,
+ Bcc?: string,
+ In-reply-to: string,
+ References: string
+}
diff --git a/emacs/Makefile.local b/emacs/Makefile.local
index 4fee0e89..fb82247f 100644
--- a/emacs/Makefile.local
+++ b/emacs/Makefile.local
@@ -13,6 +13,7 @@ emacs_sources := \
$(dir)/notmuch-maildir-fcc.el \
$(dir)/notmuch-message.el \
$(dir)/notmuch-crypto.el \
+ $(dir)/notmuch-tag.el \
$(dir)/coolj.el \
$(dir)/notmuch-print.el
diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el
index d17a30f9..684bedcb 100644
--- a/emacs/notmuch-hello.el
+++ b/emacs/notmuch-hello.el
@@ -41,7 +41,7 @@
(defun notmuch-sort-saved-searches (alist)
"Generate an alphabetically sorted saved searches alist."
- (sort alist (lambda (a b) (string< (car a) (car b)))))
+ (sort (copy-sequence alist) (lambda (a b) (string< (car a) (car b)))))
(defcustom notmuch-saved-search-sort-function nil
"Function used to sort the saved searches for the notmuch-hello view.
@@ -154,6 +154,108 @@ International Bureau of Weights and Measures."
(defvar notmuch-hello-url "http://notmuchmail.org"
"The `notmuch' web site.")
+(defvar notmuch-hello-search-pos nil
+ "Position of search widget, if any.
+
+This should only be set by `notmuch-hello-insert-search'.")
+
+(defvar notmuch-hello-custom-section-options
+ '((:filter (string :tag "Filter for each tag"))
+ (:filter-count (string :tag "Different filter to generate message counts"))
+ (:initially-hidden (const :tag "Hide this section on startup" t))
+ (:show-empty-searches (const :tag "Show queries with no matching messages" t))
+ (:hide-if-empty (const :tag "Hide this section if all queries are empty
+\(and not shown by show-empty-searches)" t)))
+ "Various customization-options for notmuch-hello-tags/query-section.")
+
+(define-widget 'notmuch-hello-tags-section 'lazy
+ "Customize-type for notmuch-hello tag-list sections."
+ :tag "Customized tag-list section (see docstring for details)"
+ :type
+ `(list :tag ""
+ (const :tag "" notmuch-hello-insert-tags-section)
+ (string :tag "Title for this section")
+ (plist
+ :inline t
+ :options
+ ,(append notmuch-hello-custom-section-options
+ '((:hide-tags (repeat :tag "Tags that will be hidden"
+ string)))))))
+
+(define-widget 'notmuch-hello-query-section 'lazy
+ "Customize-type for custom saved-search-like sections"
+ :tag "Customized queries section (see docstring for details)"
+ :type
+ `(list :tag ""
+ (const :tag "" notmuch-hello-insert-searches)
+ (string :tag "Title for this section")
+ (repeat :tag "Queries"
+ (cons (string :tag "Name") (string :tag "Query")))
+ (plist :inline t :options ,notmuch-hello-custom-section-options)))
+
+(defcustom notmuch-hello-sections
+ (list #'notmuch-hello-insert-header
+ #'notmuch-hello-insert-saved-searches
+ #'notmuch-hello-insert-search
+ #'notmuch-hello-insert-recent-searches
+ #'notmuch-hello-insert-alltags
+ #'notmuch-hello-insert-footer)
+ "Sections for notmuch-hello.
+
+The list contains functions which are used to construct sections in
+notmuch-hello buffer. When notmuch-hello buffer is constructed,
+these functions are run in the order they appear in this list. Each
+function produces a section simply by adding content to the current
+buffer. A section should not end with an empty line, because a
+newline will be inserted after each section by `notmuch-hello'.
+
+Each function should take no arguments. If the produced section
+includes `notmuch-hello-target' (i.e. cursor should be positioned
+inside this section), the function should return this element's
+position.
+Otherwise, it should return nil.
+
+For convenience an element can also be a list of the form (FUNC ARG1
+ARG2 .. ARGN) in which case FUNC will be applied to the rest of the
+list.
+
+A \"Customized tag-list section\" item in the customize-interface
+displays a list of all tags, optionally hiding some of them. It
+is also possible to filter the list of messages matching each tag
+by an additional filter query. Similarly, the count of messages
+displayed next to the buttons can be generated by applying a
+different filter to the tag query. These filters are also
+supported for \"Customized queries section\" items."
+ :group 'notmuch-hello
+ :type
+ '(repeat
+ (choice (function-item notmuch-hello-insert-header)
+ (function-item notmuch-hello-insert-saved-searches)
+ (function-item notmuch-hello-insert-search)
+ (function-item notmuch-hello-insert-recent-searches)
+ (function-item notmuch-hello-insert-alltags)
+ (function-item notmuch-hello-insert-footer)
+ (function-item notmuch-hello-insert-inbox)
+ notmuch-hello-tags-section
+ notmuch-hello-query-section
+ (function :tag "Custom section"))))
+
+(defvar notmuch-hello-target nil
+ "Button text at position of point before rebuilding the notmuch-buffer.
+
+This variable contains the text of the button, if any, the
+point was positioned at before the notmuch-hello buffer was
+rebuilt. This should never actually be global and is defined as a
+defvar only for documentation purposes and to avoid a compiler
+warning about it occurring as a free variable.")
+
+(defvar notmuch-hello-hidden-sections nil
+ "List of sections titles whose contents are hidden")
+
+(defvar notmuch-hello-first-run t
+ "True if `notmuch-hello' is run for the first time, set to nil
+afterwards.")
+
(defun notmuch-hello-nice-number (n)
(let (result)
(while (> n 0)
@@ -201,8 +303,8 @@ International Bureau of Weights and Measures."
(message "Saved '%s' as '%s'." search name)
(notmuch-hello-update)))
-(defun notmuch-hello-longest-label (tag-alist)
- (or (loop for elem in tag-alist
+(defun notmuch-hello-longest-label (searches-alist)
+ (or (loop for elem in searches-alist
maximize (length (car elem)))
0))
@@ -266,12 +368,71 @@ should be. Returns a cons cell `(tags-per-line width)'."
(* tags-per-line (+ 9 1))))
tags-per-line))))
-(defun notmuch-hello-insert-tags (tag-alist widest target)
- (let* ((tags-and-width (notmuch-hello-tags-per-line widest))
+(defun notmuch-hello-filtered-query (query filter)
+ "Constructs a query to search all messages matching QUERY and FILTER.
+
+If FILTER is a string, it is directly used in the returned query.
+
+If FILTER is a function, it is called with QUERY as a parameter and
+the string it returns is used as the query. If nil is returned,
+the entry is hidden.
+
+Otherwise, FILTER is ignored.
+"
+ (cond
+ ((functionp filter) (funcall filter query))
+ ((stringp filter)
+ (concat "(" query ") and (" filter ")"))
+ (t query)))
+
+(defun notmuch-hello-query-counts (query-alist &rest options)
+ "Compute list of counts of matched messages from QUERY-ALIST.
+
+QUERY-ALIST must be a list containing elements of the form (NAME . QUERY)
+or (NAME QUERY COUNT-QUERY). If the latter form is used,
+COUNT-QUERY specifies an alternate query to be used to generate
+the count for the associated query.
+
+The result is the list of elements of the form (NAME QUERY COUNT).
+
+The values :show-empty-searches, :filter and :filter-count from
+options will be handled as specified for
+`notmuch-hello-insert-searches'."
+ (notmuch-remove-if-not
+ #'identity
+ (mapcar
+ (lambda (elem)
+ (let* ((name (car elem))
+ (query-and-count (if (consp (cdr elem))
+ ;; do we have a different query for the message count?
+ (cons (second elem) (third elem))
+ (cons (cdr elem) (cdr elem))))
+ (message-count
+ (string-to-number
+ (notmuch-saved-search-count
+ (notmuch-hello-filtered-query (cdr query-and-count)
+ (or (plist-get options :filter-count)
+ (plist-get options :filter)))))))
+ (and (or (plist-get options :show-empty-searches) (> message-count 0))
+ (list name (notmuch-hello-filtered-query
+ (car query-and-count) (plist-get options :filter))
+ message-count))))
+ query-alist)))
+
+(defun notmuch-hello-insert-buttons (searches)
+ "Insert buttons for SEARCHES.
+
+SEARCHES must be a list containing lists of the form (NAME QUERY COUNT), where
+QUERY is the query to start when the button for the corresponding entry is
+activated. COUNT should be the number of messages matching the query.
+Such a list can be computed with `notmuch-hello-query-counts'."
+ (let* ((widest (notmuch-hello-longest-label searches))
+ (tags-and-width (notmuch-hello-tags-per-line widest))
(tags-per-line (car tags-and-width))
- (widest (cdr tags-and-width))
+ (column-width (cdr tags-and-width))
+ (column-indent 0)
(count 0)
- (reordered-list (notmuch-hello-reflect tag-alist tags-per-line))
+ (reordered-list (notmuch-hello-reflect searches tags-per-line))
;; Hack the display of the buttons used.
(widget-push-button-prefix "")
(widget-push-button-suffix "")
@@ -281,32 +442,25 @@ should be. Returns a cons cell `(tags-per-line width)'."
(mapc (lambda (elem)
;; (not elem) indicates an empty slot in the matrix.
(when elem
- (let* ((name (car elem))
- (query (cdr elem))
- (formatted-name (format "%s " name)))
+ (if (> column-indent 0)
+ (widget-insert (make-string column-indent ? )))
+ (let* ((name (first elem))
+ (query (second elem))
+ (msg-count (third elem)))
(widget-insert (format "%8s "
- (notmuch-hello-nice-number
- (string-to-number (notmuch-saved-search-count query)))))
- (if (string= formatted-name target)
+ (notmuch-hello-nice-number msg-count)))
+ (if (string= name notmuch-hello-target)
(setq found-target-pos (point-marker)))
(widget-create 'push-button
:notify #'notmuch-hello-widget-search
:notmuch-search-terms query
- formatted-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)))
- ? )))))
+ name)
+ (setq column-indent
+ (1+ (max 0 (- column-width (length name)))))))
(setq count (1+ count))
- (if (eq (% count tags-per-line) 0)
- (widget-insert "\n")))
+ (when (eq (% count tags-per-line) 0)
+ (setq column-indent 0)
+ (widget-insert "\n")))
reordered-list)
;; If the last line was not full (and hence did not include a
@@ -359,29 +513,241 @@ Complete list of currently available key bindings:
(kill-all-local-variables)
(use-local-map notmuch-hello-mode-map)
(setq major-mode 'notmuch-hello-mode
- mode-name "notmuch-hello")
+ mode-name "notmuch-hello")
(run-mode-hooks 'notmuch-hello-mode-hook)
;;(setq buffer-read-only t)
)
-(defun notmuch-hello-generate-tag-alist ()
+(defun notmuch-hello-generate-tag-alist (&optional hide-tags)
"Return an alist from tags to queries to display in the all-tags section."
- (notmuch-remove-if-not
- #'cdr
- (mapcar (lambda (tag)
- (cons tag
- (cond
- ((functionp notmuch-hello-tag-list-make-query)
- (concat "tag:" tag " and ("
- (funcall notmuch-hello-tag-list-make-query tag) ")"))
- ((stringp notmuch-hello-tag-list-make-query)
- (concat "tag:" tag " and ("
- notmuch-hello-tag-list-make-query ")"))
- (t (concat "tag:" tag)))))
- (notmuch-remove-if-not
- (lambda (tag)
- (not (member tag notmuch-hello-hide-tags)))
- (process-lines notmuch-command "search-tags")))))
+ (mapcar (lambda (tag)
+ (cons tag (format "tag:%s" tag)))
+ (notmuch-remove-if-not
+ (lambda (tag)
+ (not (member tag hide-tags)))
+ (process-lines notmuch-command "search-tags"))))
+
+(defun notmuch-hello-insert-header ()
+ "Insert the default notmuch-hello header."
+ (when notmuch-show-logo
+ (let ((image notmuch-hello-logo))
+ ;; The notmuch logo uses transparency. That can display poorly
+ ;; when inserting the image into an emacs buffer (black logo on
+ ;; a black background), so force the background colour of the
+ ;; image. We use a face to represent the colour so that
+ ;; `defface' can be used to declare the different possible
+ ;; colours, which depend on whether the frame has a light or
+ ;; dark background.
+ (setq image (cons 'image
+ (append (cdr image)
+ (list :background (face-background 'notmuch-hello-logo-background)))))
+ (insert-image image))
+ (widget-insert " "))
+
+ (widget-insert "Welcome to ")
+ ;; Hack the display of the links used.
+ (let ((widget-link-prefix "")
+ (widget-link-suffix ""))
+ (widget-create 'link
+ :notify (lambda (&rest ignore)
+ (browse-url notmuch-hello-url))
+ :help-echo "Visit the notmuch website."
+ "notmuch")
+ (widget-insert ". ")
+ (widget-insert "You have ")
+ (widget-create 'link
+ :notify (lambda (&rest ignore)
+ (notmuch-hello-update))
+ :help-echo "Refresh"
+ (notmuch-hello-nice-number
+ (string-to-number (car (process-lines notmuch-command "count")))))
+ (widget-insert " messages.\n")))
+
+
+(defun notmuch-hello-insert-saved-searches ()
+ "Insert the saved-searches section."
+ (let ((searches (notmuch-hello-query-counts
+ (if notmuch-saved-search-sort-function
+ (funcall notmuch-saved-search-sort-function
+ notmuch-saved-searches)
+ notmuch-saved-searches)
+ :show-empty-searches notmuch-show-empty-saved-searches))
+ found-target-pos)
+ (when searches
+ (widget-insert "Saved searches: ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (customize-variable 'notmuch-saved-searches))
+ "edit")
+ (widget-insert "\n\n")
+ (let ((start (point)))
+ (setq found-target-pos
+ (notmuch-hello-insert-buttons searches))
+ (indent-rigidly start (point) notmuch-hello-indent)
+ found-target-pos))))
+
+(defun notmuch-hello-insert-search ()
+ "Insert a search widget."
+ (widget-insert "Search: ")
+ (setq notmuch-hello-search-pos (point-marker))
+ (widget-create 'editable-field
+ ;; Leave some space at the start and end of the
+ ;; search boxes.
+ :size (max 8 (- (window-width) notmuch-hello-indent
+ (length "Search: ")))
+ :action (lambda (widget &rest ignore)
+ (notmuch-hello-search (widget-value widget))))
+ ;; 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"))
+
+(defun notmuch-hello-insert-recent-searches ()
+ "Insert recent searches."
+ (when notmuch-search-history
+ (widget-insert "Recent searches: ")
+ (widget-create 'push-button
+ :notify (lambda (&rest ignore)
+ (setq notmuch-search-history nil)
+ (notmuch-hello-update))
+ "clear")
+ (widget-insert "\n\n")
+ (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
+ ;; less than 8 characters wide.
+ :size (max 8
+ (- (window-width)
+ ;; Leave some space
+ ;; at the start and
+ ;; end of the
+ ;; boxes.
+ (* 2 notmuch-hello-indent)
+ ;; 1 for the space
+ ;; before the
+ ;; `[save]' button. 6
+ ;; for the `[save]'
+ ;; button.
+ 1 6))
+ :action (lambda (widget &rest ignore)
+ (notmuch-hello-search (widget-value widget)))
+ search))
+ (widget-insert " ")
+ (widget-create 'push-button
+ :notify (lambda (widget &rest ignore)
+ (notmuch-hello-add-saved-search widget))
+ :notmuch-saved-search-widget widget-symbol
+ "save"))
+ (widget-insert "\n"))
+ (indent-rigidly start (point) notmuch-hello-indent))
+ nil))
+
+(defun notmuch-hello-insert-searches (title query-alist &rest options)
+ "Insert a section with TITLE showing a list of buttons made from QUERY-ALIST.
+
+QUERY-ALIST must be a list containing elements of the form (NAME . QUERY)
+or (NAME QUERY COUNT-QUERY). If the latter form is used,
+COUNT-QUERY specifies an alternate query to be used to generate
+the count for the associated item.
+
+Supports the following entries in OPTIONS as a plist:
+:initially-hidden - if non-nil, section will be hidden on startup
+:show-empty-searches - show buttons with no matching messages
+:hide-if-empty - hide if no buttons would be shown
+ (only makes sense without :show-empty-searches)
+:filter - This can be a function that takes the search query as its argument and
+ returns a filter to be used in conjuction with the query for that search or nil
+ to hide the element. This can also be a string that is used as a combined with
+ each query using \"and\".
+:filter-count - Separate filter to generate the count displayed each search. Accepts
+ the same values as :filter. If :filter and :filter-count are specified, this
+ will be used instead of :filter, not in conjunction with it."
+ (widget-insert title ": ")
+ (if (and notmuch-hello-first-run (plist-get options :initially-hidden))
+ (add-to-list 'notmuch-hello-hidden-sections title))
+ (let ((is-hidden (member title notmuch-hello-hidden-sections))
+ (start (point)))
+ (if is-hidden
+ (widget-create 'push-button
+ :notify `(lambda (widget &rest ignore)
+ (setq notmuch-hello-hidden-sections
+ (delete ,title notmuch-hello-hidden-sections))
+ (notmuch-hello-update))
+ "show")
+ (widget-create 'push-button
+ :notify `(lambda (widget &rest ignore)
+ (add-to-list 'notmuch-hello-hidden-sections
+ ,title)
+ (notmuch-hello-update))
+ "hide"))
+ (widget-insert "\n")
+ (let (target-pos)
+ (when (not is-hidden)
+ (let ((searches (apply 'notmuch-hello-query-counts query-alist options)))
+ (when (or (not (plist-get options :hide-if-empty))
+ searches)
+ (widget-insert "\n")
+ (setq target-pos
+ (notmuch-hello-insert-buttons searches))
+ (indent-rigidly start (point) notmuch-hello-indent))))
+ target-pos)))
+
+(defun notmuch-hello-insert-tags-section (&optional title &rest options)
+ "Insert a section displaying all tags with message counts.
+
+TITLE defaults to \"All tags\".
+Allowed options are those accepted by `notmuch-hello-insert-searches' and the
+following:
+
+:hide-tags - List of tags that should be excluded."
+ (apply 'notmuch-hello-insert-searches
+ (or title "All tags")
+ (notmuch-hello-generate-tag-alist (plist-get options :hide-tags))
+ options))
+
+(defun notmuch-hello-insert-inbox ()
+ "Show an entry for each saved search and inboxed messages for each tag"
+ (notmuch-hello-insert-searches "What's in your inbox"
+ (append
+ (notmuch-saved-searches)
+ (notmuch-hello-generate-tag-alist))
+ :filter "tag:inbox"))
+
+(defun notmuch-hello-insert-alltags ()
+ "Insert a section displaying all tags and associated message counts"
+ (notmuch-hello-insert-tags-section
+ nil
+ :initially-hidden (not notmuch-show-all-tags-list)
+ :hide-tags notmuch-hello-hide-tags
+ :filter notmuch-hello-tag-list-make-query))
+
+(defun notmuch-hello-insert-footer ()
+ "Insert the notmuch-hello footer."
+ (let ((start (point)))
+ (widget-insert "Type a search query and hit RET to view matching threads.\n")
+ (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 "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
+ (widget-create 'link
+ :notify (lambda (&rest ignore)
+ (customize-variable 'notmuch-hello-sections))
+ :button-prefix "" :button-suffix ""
+ "Customize")
+ (widget-insert " this page.")
+ (let ((fill-column (- (window-width) notmuch-hello-indent)))
+ (center-region start (point)))))
;;;###autoload
(defun notmuch-hello (&optional no-display)
@@ -397,13 +763,13 @@ Complete list of currently available key bindings:
(set-buffer "*notmuch-hello*")
(switch-to-buffer "*notmuch-hello*"))
- (let ((target (if (widget-at)
- (widget-value (widget-at))
- (condition-case nil
- (progn
- (widget-forward 1)
- (widget-value (widget-at)))
- (error nil))))
+ (let ((notmuch-hello-target (if (widget-at)
+ (widget-value (widget-at))
+ (condition-case nil
+ (progn
+ (widget-forward 1)
+ (widget-value (widget-at)))
+ (error nil))))
(inhibit-read-only t))
;; Delete all editable widget fields. Editable widget fields are
@@ -422,168 +788,20 @@ Complete list of currently available key bindings:
(mapc 'delete-overlay (car all))
(mapc 'delete-overlay (cdr all)))
- (when notmuch-show-logo
- (let ((image notmuch-hello-logo))
- ;; The notmuch logo uses transparency. That can display poorly
- ;; when inserting the image into an emacs buffer (black logo on
- ;; a black background), so force the background colour of the
- ;; image. We use a face to represent the colour so that
- ;; `defface' can be used to declare the different possible
- ;; colours, which depend on whether the frame has a light or
- ;; dark background.
- (setq image (cons 'image
- (append (cdr image)
- (list :background (face-background 'notmuch-hello-logo-background)))))
- (insert-image image))
- (widget-insert " "))
-
- (widget-insert "Welcome to ")
- ;; Hack the display of the links used.
- (let ((widget-link-prefix "")
- (widget-link-suffix ""))
- (widget-create 'link
- :notify (lambda (&rest ignore)
- (browse-url notmuch-hello-url))
- :help-echo "Visit the notmuch website."
- "notmuch")
- (widget-insert ". ")
- (widget-insert "You have ")
- (widget-create 'link
- :notify (lambda (&rest ignore)
- (notmuch-hello-update))
- :help-echo "Refresh"
- (notmuch-hello-nice-number
- (string-to-number (car (process-lines notmuch-command "count")))))
- (widget-insert " messages.\n"))
-
- (let ((found-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
- notmuch-saved-searches
- (loop for elem in notmuch-saved-searches
- if (> (string-to-number (notmuch-saved-search-count (cdr elem))) 0)
- collect elem)))
- (saved-widest (notmuch-hello-longest-label saved-alist))
- (alltags-alist (if notmuch-show-all-tags-list (notmuch-hello-generate-tag-alist)))
- (alltags-widest (notmuch-hello-longest-label alltags-alist))
- (widest (max saved-widest alltags-widest)))
-
- (when saved-alist
- ;; Sort saved searches if required.
- (when notmuch-saved-search-sort-function
- (setq saved-alist
- (funcall notmuch-saved-search-sort-function saved-alist)))
- (widget-insert "\nSaved searches: ")
- (widget-create 'push-button
- :notify (lambda (&rest ignore)
- (customize-variable 'notmuch-saved-searches))
- "edit")
- (widget-insert "\n\n")
- (setq final-target-pos (point-marker))
- (let ((start (point)))
- (setq found-target-pos (notmuch-hello-insert-tags saved-alist widest target))
- (if found-target-pos
- (setq final-target-pos found-target-pos))
- (indent-rigidly start (point) notmuch-hello-indent)))
-
- (widget-insert "\nSearch: ")
- (setq default-pos (point-marker))
- (widget-create 'editable-field
- ;; Leave some space at the start and end of the
- ;; search boxes.
- :size (max 8 (- (window-width) notmuch-hello-indent
- (length "Search: ")))
- :action (lambda (widget &rest ignore)
- (notmuch-hello-search (widget-value widget))))
- ;; 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-search-history
- (widget-insert "\nRecent searches: ")
- (widget-create 'push-button
- :notify (lambda (&rest ignore)
- (setq notmuch-search-history nil)
- (notmuch-hello-update))
- "clear")
- (widget-insert "\n\n")
- (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
- ;; less than 8 characters wide.
- :size (max 8
- (- (window-width)
- ;; Leave some space
- ;; at the start and
- ;; end of the
- ;; boxes.
- (* 2 notmuch-hello-indent)
- ;; 1 for the space
- ;; before the
- ;; `[save]' button. 6
- ;; for the `[save]'
- ;; button.
- 1 6))
- :action (lambda (widget &rest ignore)
- (notmuch-hello-search (widget-value widget)))
- search))
- (widget-insert " ")
- (widget-create 'push-button
- :notify (lambda (widget &rest ignore)
- (notmuch-hello-add-saved-search widget))
- :notmuch-saved-search-widget widget-symbol
- "save"))
- (widget-insert "\n"))
- (indent-rigidly start (point) notmuch-hello-indent)))
-
- (when alltags-alist
- (widget-insert "\nAll tags: ")
- (widget-create 'push-button
- :notify (lambda (widget &rest ignore)
- (setq notmuch-show-all-tags-list nil)
- (notmuch-hello-update))
- "hide")
- (widget-insert "\n\n")
- (let ((start (point)))
- (setq found-target-pos (notmuch-hello-insert-tags alltags-alist widest target))
- (unless final-target-pos
- (setq final-target-pos found-target-pos))
- (indent-rigidly start (point) notmuch-hello-indent)))
-
- (widget-insert "\n")
-
- (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-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 "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
- (let ((fill-column (- (window-width) notmuch-hello-indent)))
- (center-region start (point))))
-
+ (let (final-target-pos)
+ (mapc
+ (lambda (section)
+ (let ((point-before (point))
+ (result (if (functionp section)
+ (funcall section)
+ (apply (car section) (cdr section)))))
+ (if (and (not final-target-pos) (integer-or-marker-p result))
+ (setq final-target-pos result))
+ ;; don't insert a newline when the previous section didn't show
+ ;; anything.
+ (unless (eq (point) point-before)
+ (widget-insert "\n"))))
+ notmuch-hello-sections)
(widget-setup)
(when final-target-pos
@@ -592,9 +810,10 @@ Complete list of currently available key bindings:
(widget-forward 1)))
(unless (widget-at)
- (goto-char default-pos))))
-
- (run-hooks 'notmuch-hello-refresh-hook))
+ (when notmuch-hello-search-pos
+ (goto-char notmuch-hello-search-pos)))))
+ (run-hooks 'notmuch-hello-refresh-hook)
+ (setq notmuch-hello-first-run nil))
(defun notmuch-folder ()
"Deprecated function for invoking notmuch---calling `notmuch' is preferred now."
diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
index d315f765..e99b48d1 100644
--- a/emacs/notmuch-lib.el
+++ b/emacs/notmuch-lib.el
@@ -21,6 +21,10 @@
;; This is an part of an emacs-based interface to the notmuch mail system.
+(require 'mm-view)
+(require 'mm-decode)
+(eval-when-compile (require 'cl))
+
(defvar notmuch-command "notmuch"
"Command to run the notmuch binary.")
@@ -142,6 +146,10 @@ the user hasn't set this variable with the old or new value."
"[No Subject]"
subject)))
+(defun notmuch-id-to-query (id)
+ "Return a query that matches the message with id ID."
+ (concat "id:\"" (replace-regexp-in-string "\"" "\"\"" id t t) "\""))
+
;;
(defun notmuch-common-do-stash (text)
@@ -173,6 +181,93 @@ the user hasn't set this variable with the old or new value."
(list 'when (< emacs-major-version 23)
form))
+(defun notmuch-split-content-type (content-type)
+ "Split content/type into 'content' and 'type'"
+ (split-string content-type "/"))
+
+(defun notmuch-match-content-type (t1 t2)
+ "Return t if t1 and t2 are matching content types, taking wildcards into account"
+ (let ((st1 (notmuch-split-content-type t1))
+ (st2 (notmuch-split-content-type t2)))
+ (if (or (string= (cadr st1) "*")
+ (string= (cadr st2) "*"))
+ ;; Comparison of content types should be case insensitive.
+ (string= (downcase (car st1)) (downcase (car st2)))
+ (string= (downcase t1) (downcase t2)))))
+
+(defvar notmuch-multipart/alternative-discouraged
+ '(
+ ;; Avoid HTML parts.
+ "text/html"
+ ;; multipart/related usually contain a text/html part and some associated graphics.
+ "multipart/related"
+ ))
+
+(defun notmuch-multipart/alternative-choose (types)
+ "Return a list of preferred types from the given list of types"
+ ;; Based on `mm-preferred-alternative-precedence'.
+ (let ((seq types))
+ (dolist (pref (reverse notmuch-multipart/alternative-discouraged))
+ (dolist (elem (copy-sequence seq))
+ (when (string-match pref elem)
+ (setq seq (nconc (delete elem seq) (list elem))))))
+ seq))
+
+(defun notmuch-parts-filter-by-type (parts type)
+ "Given a list of message parts, return a list containing the ones matching
+the given type."
+ (remove-if-not
+ (lambda (part) (notmuch-match-content-type (plist-get part :content-type) type))
+ parts))
+
+;; Helper for parts which are generally not included in the default
+;; JSON output.
+(defun notmuch-get-bodypart-internal (query part-number process-crypto)
+ (let ((args '("show" "--format=raw"))
+ (part-arg (format "--part=%s" part-number)))
+ (setq args (append args (list part-arg)))
+ (if process-crypto
+ (setq args (append args '("--decrypt"))))
+ (setq args (append args (list query)))
+ (with-temp-buffer
+ (let ((coding-system-for-read 'no-conversion))
+ (progn
+ (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
+ (buffer-string))))))
+
+(defun notmuch-get-bodypart-content (msg part nth process-crypto)
+ (or (plist-get part :content)
+ (notmuch-get-bodypart-internal (notmuch-id-to-query (plist-get msg :id)) nth process-crypto)))
+
+(defun notmuch-mm-display-part-inline (msg part nth content-type process-crypto)
+ "Use the mm-decode/mm-view functions to display a part in the
+current buffer, if possible."
+ (let ((display-buffer (current-buffer)))
+ (with-temp-buffer
+ ;; In case there is :content, the content string is already converted
+ ;; into emacs internal format. `gnus-decoded' is a fake charset,
+ ;; which means no further decoding (to be done by mm- functions).
+ (let* ((charset (if (plist-member part :content)
+ 'gnus-decoded
+ (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-get-bodypart-content msg part nth process-crypto))
+ (when (mm-inlinable-p handle)
+ (set-buffer display-buffer)
+ (mm-display-part handle)
+ t))))))
+
+;; Converts a plist of headers to an alist of headers. The input plist should
+;; have symbols of the form :Header as keys, and the resulting alist will have
+;; symbols of the form 'Header as keys.
+(defun notmuch-headers-plist-to-alist (plist)
+ (loop for (key value . rest) on plist by #'cddr
+ collect (cons (intern (substring (symbol-name key) 1)) value)))
+
;; Compatibility functions for versions of emacs before emacs 23.
;;
;; Both functions here were copied from emacs 23 with the following copyright:
diff --git a/emacs/notmuch-message.el b/emacs/notmuch-message.el
index 264a5b9b..5964caa3 100644
--- a/emacs/notmuch-message.el
+++ b/emacs/notmuch-message.el
@@ -20,6 +20,7 @@
;; Authors: Jesse Rosenthal <jrosenthal@jhu.edu>
(require 'message)
+(require 'notmuch-tag)
(require 'notmuch-mua)
(defcustom notmuch-message-replied-tags '("replied")
@@ -44,7 +45,7 @@ the \"inbox\" and \"todo\", you would set
(concat "+" str)
str))
notmuch-message-replied-tags)))
- (apply 'notmuch-tag (concat "id:" (car (car rep))) tags)))))
+ (apply 'notmuch-tag (notmuch-id-to-query (car (car rep))) tags)))))
(add-hook 'message-send-hook 'notmuch-message-mark-replied)
diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el
index 13244eb8..408b49e0 100644
--- a/emacs/notmuch-mua.el
+++ b/emacs/notmuch-mua.el
@@ -19,11 +19,16 @@
;;
;; Authors: David Edmondson <dme@dme.org>
+(require 'json)
(require 'message)
+(require 'mm-view)
+(require 'format-spec)
(require 'notmuch-lib)
(require 'notmuch-address)
+(eval-when-compile (require 'cl))
+
;;
(defcustom notmuch-mua-send-hook '(notmuch-mua-message-send-hook)
@@ -32,6 +37,26 @@
:group 'notmuch-send
:group 'notmuch-hooks)
+(defcustom notmuch-mua-compose-in 'current-window
+ (concat
+ "Where to create the mail buffer used to compose a new message.
+Possible values are `current-window' (default), `new-window' and
+`new-frame'. If set to `current-window', the mail buffer will be
+displayed in the current window, so the old buffer will be
+restored when the mail buffer is killed. If set to `new-window'
+or `new-frame', the mail buffer will be displayed in a new
+window/frame that will be destroyed when the buffer is killed.
+You may want to customize `message-kill-buffer-on-exit'
+accordingly."
+ (when (< emacs-major-version 24)
+ " Due to a known bug in Emacs 23, you should not set
+this to `new-window' if `message-kill-buffer-on-exit' is
+disabled: this would result in an incorrect behavior."))
+ :group 'notmuch-send
+ :type '(choice (const :tag "Compose in the current window" current-window)
+ (const :tag "Compose mail in a new window" new-window)
+ (const :tag "Compose mail in a new frame" new-frame)))
+
(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."
@@ -51,6 +76,23 @@ list."
;;
+(defun notmuch-mua-get-switch-function ()
+ "Get a switch function according to `notmuch-mua-compose-in'."
+ (cond ((eq notmuch-mua-compose-in 'current-window)
+ 'switch-to-buffer)
+ ((eq notmuch-mua-compose-in 'new-window)
+ 'switch-to-buffer-other-window)
+ ((eq notmuch-mua-compose-in 'new-frame)
+ 'switch-to-buffer-other-frame)
+ (t (error "Invalid value for `notmuch-mua-compose-in'"))))
+
+(defun notmuch-mua-maybe-set-window-dedicated ()
+ "Set the selected window as dedicated according to
+`notmuch-mua-compose-in'."
+ (when (or (eq notmuch-mua-compose-in 'new-frame)
+ (eq notmuch-mua-compose-in 'new-window))
+ (set-window-dedicated-p (selected-window) t)))
+
(defun notmuch-mua-user-agent-full ()
"Generate a `User-Agent:' string suitable for notmuch."
(concat (notmuch-mua-user-agent-notmuch)
@@ -72,56 +114,122 @@ list."
(push header message-hidden-headers)))
notmuch-mua-hidden-headers))
+(defun notmuch-mua-get-quotable-parts (parts)
+ (loop for part in parts
+ if (notmuch-match-content-type (plist-get part :content-type) "multipart/alternative")
+ collect (let* ((subparts (plist-get part :content))
+ (types (mapcar (lambda (part) (plist-get part :content-type)) subparts))
+ (chosen-type (car (notmuch-multipart/alternative-choose types))))
+ (loop for part in (reverse subparts)
+ if (notmuch-match-content-type (plist-get part :content-type) chosen-type)
+ return part))
+ else if (notmuch-match-content-type (plist-get part :content-type) "multipart/*")
+ append (notmuch-mua-get-quotable-parts (plist-get part :content))
+ else if (notmuch-match-content-type (plist-get part :content-type) "text/*")
+ collect part))
+
+(defun notmuch-mua-insert-quotable-part (message part)
+ (save-restriction
+ (narrow-to-region (point) (point))
+ (notmuch-mm-display-part-inline message part (plist-get part :id)
+ (plist-get part :content-type)
+ notmuch-show-process-crypto)
+ (goto-char (point-max))))
+
+;; There is a bug in emacs 23's message.el that results in a newline
+;; not being inserted after the References header, so the next header
+;; is concatenated to the end of it. This function fixes the problem,
+;; while guarding against the possibility that some current or future
+;; version of emacs has the bug fixed.
+(defun notmuch-mua-insert-references (original-func header references)
+ (funcall original-func header references)
+ (unless (bolp) (insert "\n")))
+
(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"))))
+ (let ((args '("reply" "--format=json"))
+ reply
+ original)
+ (when 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
- ;; line and then the body.
+
+ ;; Get the reply object as JSON, and parse it into an elisp object.
(with-temp-buffer
- (apply 'call-process (append (list notmuch-command nil (list t t) nil) args))
+ (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
(goto-char (point-min))
- (if (re-search-forward "^$" nil t)
- (save-excursion
- (save-restriction
- (narrow-to-region (point-min) (point))
- (goto-char (point-min))
- (setq headers (mail-header-extract)))))
- (forward-line 1)
- ;; Original message may contain (malicious) MML tags. We must
- ;; properly quote them in the reply.
- (mml-quote-region (point) (point-max))
- (setq body (buffer-substring (point) (point-max))))
- ;; If sender is non-nil, set the From: header to its value.
- (when sender
- (mail-header-set 'from sender headers))
- (let
- ;; Overlay the composition window on that being used to read
- ;; the original message.
- ((same-window-regexps '("\\*mail .*")))
- (notmuch-mua-mail (mail-header 'to headers)
- (mail-header 'subject headers)
- (message-headers-to-generate headers t '(to subject))))
- ;; insert the message body - but put it in front of the signature
- ;; if one is present
- (goto-char (point-max))
- (if (re-search-backward message-signature-separator nil t)
+ (let ((json-object-type 'plist)
+ (json-array-type 'list)
+ (json-false 'nil))
+ (setq reply (json-read))))
+
+ ;; Extract the original message to simplify the following code.
+ (setq original (plist-get reply :original))
+
+ ;; Extract the headers of both the reply and the original message.
+ (let* ((original-headers (plist-get original :headers))
+ (reply-headers (plist-get reply :reply-headers)))
+
+ ;; If sender is non-nil, set the From: header to its value.
+ (when sender
+ (plist-put reply-headers :From sender))
+ (let
+ ;; Overlay the composition window on that being used to read
+ ;; the original message.
+ ((same-window-regexps '("\\*mail .*")))
+
+ ;; We modify message-header-format-alist to get around a bug in message.el.
+ ;; See the comment above on notmuch-mua-insert-references.
+ (let ((message-header-format-alist
+ (loop for pair in message-header-format-alist
+ if (eq (car pair) 'References)
+ collect (cons 'References
+ (apply-partially
+ 'notmuch-mua-insert-references
+ (cdr pair)))
+ else
+ collect pair)))
+ (notmuch-mua-mail (plist-get reply-headers :To)
+ (plist-get reply-headers :Subject)
+ (notmuch-headers-plist-to-alist reply-headers)
+ nil (notmuch-mua-get-switch-function))))
+
+ ;; Insert the message body - but put it in front of the signature
+ ;; if one is present
+ (goto-char (point-max))
+ (if (re-search-backward message-signature-separator nil t)
(forward-line -1)
- (goto-char (point-max)))
- (insert body)
- (push-mark))
- (set-buffer-modified-p nil)
+ (goto-char (point-max)))
+
+ (let ((from (plist-get original-headers :From))
+ (date (plist-get original-headers :Date))
+ (start (point)))
+
+ ;; message-cite-original constructs a citation line based on the From and Date
+ ;; headers of the original message, which are assumed to be in the buffer.
+ (insert "From: " from "\n")
+ (insert "Date: " date "\n\n")
+
+ ;; Get the parts of the original message that should be quoted; this includes
+ ;; all the text parts, except the non-preferred ones in a multipart/alternative.
+ (let ((quotable-parts (notmuch-mua-get-quotable-parts (plist-get original :body))))
+ (mapc (apply-partially 'notmuch-mua-insert-quotable-part original) quotable-parts))
+
+ (set-mark (point))
+ (goto-char start)
+ ;; Quote the original message according to the user's configured style.
+ (message-cite-original))))
- (message-goto-body))
+ (goto-char (point-max))
+ (push-mark)
+ (message-goto-body)
+ (set-buffer-modified-p nil))
(defun notmuch-mua-forward-message ()
+ (funcall (notmuch-mua-get-switch-function) (current-buffer))
(message-forward)
(when notmuch-mua-user-agent-function
@@ -131,6 +239,7 @@ list."
(message-sort-headers)
(message-hide-headers)
(set-buffer-modified-p nil)
+ (notmuch-mua-maybe-set-window-dedicated)
(message-goto-to))
@@ -143,16 +252,17 @@ OTHER-ARGS are passed through to `message-mail'."
(when notmuch-mua-user-agent-function
(let ((user-agent (funcall notmuch-mua-user-agent-function)))
(when (not (string= "" user-agent))
- (push (cons "User-Agent" user-agent) other-headers))))
+ (push (cons 'User-Agent user-agent) other-headers))))
- (unless (mail-header 'from other-headers)
- (push (cons "From" (concat
- (notmuch-user-name) " <" (notmuch-user-primary-email) ">")) other-headers))
+ (unless (assq 'From other-headers)
+ (push (cons 'From (concat
+ (notmuch-user-name) " <" (notmuch-user-primary-email) ">")) other-headers))
(apply #'message-mail to subject other-headers other-args)
(message-sort-headers)
(message-hide-headers)
(set-buffer-modified-p nil)
+ (notmuch-mua-maybe-set-window-dedicated)
(message-goto-to))
@@ -208,8 +318,8 @@ the From: address first."
(interactive "P")
(let ((other-headers
(when (or prompt-for-sender notmuch-always-prompt-for-sender)
- (list (cons 'from (notmuch-mua-prompt-for-sender))))))
- (notmuch-mua-mail nil nil other-headers)))
+ (list (cons 'From (notmuch-mua-prompt-for-sender))))))
+ (notmuch-mua-mail nil nil other-headers nil (notmuch-mua-get-switch-function))))
(defun notmuch-mua-new-forward-message (&optional prompt-for-sender)
"Invoke the notmuch message forwarding window.
diff --git a/emacs/notmuch-print.el b/emacs/notmuch-print.el
index 6653d977..8c18f4bd 100644
--- a/emacs/notmuch-print.el
+++ b/emacs/notmuch-print.el
@@ -25,7 +25,7 @@
(defcustom notmuch-print-mechanism 'notmuch-print-lpr
"How should printing be done?"
- :group 'notmuch
+ :group 'notmuch-show
:type '(choice
(function :tag "Use lpr" notmuch-print-lpr)
(function :tag "Use ps-print" notmuch-print-ps-print)
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
index 7c4c0bea..d318430c 100644
--- a/emacs/notmuch-show.el
+++ b/emacs/notmuch-show.el
@@ -30,6 +30,7 @@
(require 'goto-addr)
(require 'notmuch-lib)
+(require 'notmuch-tag)
(require 'notmuch-query)
(require 'notmuch-wash)
(require 'notmuch-mua)
@@ -38,10 +39,8 @@
(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
(declare-function notmuch-fontify-headers "notmuch" nil)
-(declare-function notmuch-read-tag-changes "notmuch" (&optional initial-input &rest search-terms))
(declare-function notmuch-search-next-thread "notmuch" nil)
(declare-function notmuch-search-show-thread "notmuch" nil)
-(declare-function notmuch-update-tags "notmuch" (current-tags tag-changes))
(defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
"Headers that should be shown in a message, in this order.
@@ -488,7 +487,7 @@ message at DEPTH in the current thread."
(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))
+ (insert (notmuch-get-bodypart-internal ,message-id ,nth notmuch-show-process-crypto))
,@body))))
(defun notmuch-show-save-part (message-id nth &optional filename content-type)
@@ -525,47 +524,13 @@ message at DEPTH in the current thread."
(let ((handle (mm-make-handle (current-buffer) (list content-type))))
(mm-interactively-view-part handle))))
-(defun notmuch-show-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* ((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
- '(
- ;; Avoid HTML parts.
- "text/html"
- ;; multipart/related usually contain a text/html part and some associated graphics.
- "multipart/related"
- ))
-
(defun notmuch-show-multipart/*-to-list (part)
(mapcar (lambda (inner-part) (plist-get inner-part :content-type))
(plist-get part :content)))
-(defun notmuch-show-multipart/alternative-choose (types)
- ;; Based on `mm-preferred-alternative-precedence'.
- (let ((seq types))
- (dolist (pref (reverse notmuch-show-multipart/alternative-discouraged))
- (dolist (elem (copy-sequence seq))
- (when (string-match pref elem)
- (setq seq (nconc (delete elem seq) (list elem))))))
- seq))
-
(defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth declared-type)
(notmuch-show-insert-part-header nth declared-type content-type nil)
- (let ((chosen-type (car (notmuch-show-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
+ (let ((chosen-type (car (notmuch-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
(inner-parts (plist-get part :content))
(start (point)))
;; This inserts all parts of the chosen type rather than just one,
@@ -630,8 +595,8 @@ current buffer, if possible."
;; times (hundreds!), which results in many calls to
;; `notmuch part'.
(unless content
- (setq content (notmuch-show-get-bodypart-internal (concat "id:" message-id)
- part-number))
+ (setq content (notmuch-get-bodypart-internal (notmuch-id-to-query message-id)
+ part-number notmuch-show-process-crypto))
(with-current-buffer w3m-current-buffer
(notmuch-show-w3m-cid-store-internal url
message-id
@@ -751,7 +716,7 @@ current buffer, if possible."
;; insert a header to make this clear.
(if (> nth 1)
(notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename)))
- (insert (notmuch-show-get-bodypart-content msg part nth))
+ (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
(save-excursion
(save-restriction
(narrow-to-region start (point-max))
@@ -761,7 +726,7 @@ current buffer, if possible."
(defun notmuch-show-insert-part-text/calendar (msg part content-type nth depth declared-type)
(notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename))
(insert (with-temp-buffer
- (insert (notmuch-show-get-bodypart-content msg part nth))
+ (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
(goto-char (point-min))
(let ((file (make-temp-file "notmuch-ical"))
result)
@@ -803,14 +768,11 @@ current buffer, if possible."
(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))
- (notmuch-show-mm-display-part-inline msg part nth content-type)
+ (notmuch-mm-display-part-inline msg part nth content-type notmuch-show-process-crypto)
t)
;; Functions for determining how to handle MIME parts.
-(defun notmuch-show-split-content-type (content-type)
- (split-string content-type "/"))
-
(defun notmuch-show-handlers-for (content-type)
"Return a list of content handlers for a part of type CONTENT-TYPE."
(let (result)
@@ -821,30 +783,11 @@ current buffer, if possible."
(list (intern (concat "notmuch-show-insert-part-*/*"))
(intern (concat
"notmuch-show-insert-part-"
- (car (notmuch-show-split-content-type content-type))
+ (car (notmuch-split-content-type content-type))
"/*"))
(intern (concat "notmuch-show-insert-part-" content-type))))
result))
-;; Helper for parts which are generally not included in the default
-;; JSON output.
-(defun notmuch-show-get-bodypart-internal (message-id part-number)
- (let ((args '("show" "--format=raw"))
- (part-arg (format "--part=%s" part-number)))
- (setq args (append args (list part-arg)))
- (if notmuch-show-process-crypto
- (setq args (append args '("--decrypt"))))
- (setq args (append args (list message-id)))
- (with-temp-buffer
- (let ((coding-system-for-read 'no-conversion))
- (progn
- (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
- (buffer-string))))))
-
-(defun notmuch-show-get-bodypart-content (msg part nth)
- (or (plist-get part :content)
- (notmuch-show-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth)))
-
;;
(defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth declared-type)
@@ -981,7 +924,8 @@ current buffer, if possible."
;; Message visibility depends on whether it matched the search
;; criteria.
- (notmuch-show-message-visible msg (plist-get msg :match))))
+ (notmuch-show-message-visible msg (and (plist-get msg :match)
+ (not (plist-get msg :excluded))))))
(defun notmuch-show-toggle-process-crypto ()
"Toggle the processing of cryptographic MIME parts."
@@ -1081,11 +1025,7 @@ function is used."
notmuch-show-parent-buffer parent-buffer
notmuch-show-query-context query-context)
(notmuch-show-build-buffer)
-
- ;; Move to the first open message and mark it read
- (if (notmuch-show-message-visible-p)
- (notmuch-show-mark-read)
- (notmuch-show-next-open-message))))
+ (notmuch-show-goto-first-wanted-message)))
(defun notmuch-show-build-buffer ()
(let ((inhibit-read-only t))
@@ -1102,20 +1042,22 @@ function is used."
(append (list "\'") basic-args
(list "and (" notmuch-show-query-context ")\'"))
(append (list "\'") basic-args (list "\'")))))
- (notmuch-show-insert-forest (notmuch-query-get-threads args))
+ (notmuch-show-insert-forest (notmuch-query-get-threads
+ (cons "--exclude=false" args)))
;; If the query context reduced the results to nothing, run
;; the basic query.
(when (and (eq (buffer-size) 0)
notmuch-show-query-context)
(notmuch-show-insert-forest
- (notmuch-query-get-threads basic-args))))
+ (notmuch-query-get-threads
+ (cons "--exclude=false" basic-args)))))
(jit-lock-register #'notmuch-show-buttonise-links)
(run-hooks 'notmuch-show-hook))
;; Set the header line to the subject of the first message.
- (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-pretty-subject)))))
+ (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))))
(defun notmuch-show-capture-state ()
"Capture the state of the current buffer.
@@ -1167,9 +1109,7 @@ reset based on the original query."
(notmuch-show-apply-state state)
;; We're resetting state, so navigate to the first open message
;; and mark it read, just like opening a new show buffer.
- (if (notmuch-show-message-visible-p)
- (notmuch-show-mark-read)
- (notmuch-show-next-open-message)))))
+ (notmuch-show-goto-first-wanted-message))))
(defvar notmuch-show-stash-map
(let ((map (make-sparse-keymap)))
@@ -1369,16 +1309,16 @@ Some useful entries are:
(plist-get props prop)))
(defun notmuch-show-get-message-id (&optional bare)
- "Return the Message-Id of the current message.
+ "Return an id: query for the Message-Id of the current message.
If optional argument BARE is non-nil, return
-the Message-Id without prefix and quotes."
+the Message-Id without id: prefix and escaping."
(if bare
(notmuch-show-get-prop :id)
- (concat "id:\"" (notmuch-show-get-prop :id) "\"")))
+ (notmuch-id-to-query (notmuch-show-get-prop :id))))
(defun notmuch-show-get-messages-ids ()
- "Return all message ids of messages in the current thread."
+ "Return all id: queries of messages in the current thread."
(let ((message-ids))
(notmuch-show-mapc
(lambda () (push (notmuch-show-get-message-id) message-ids)))
@@ -1417,9 +1357,6 @@ current thread."
(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)
@@ -1445,7 +1382,7 @@ current thread."
;; thread.
(defun notmuch-show-get-message-ids-for-open-messages ()
- "Return a list of all message IDs for open messages in the current thread."
+ "Return a list of all id: queries for open messages in the current thread."
(save-excursion
(let (message-ids done)
(goto-char (point-min))
@@ -1489,6 +1426,11 @@ current window), advance to the next open message."
;; This is not the last message - move to the next visible one.
(notmuch-show-next-open-message))
+ ((not (= (point) (point-max)))
+ ;; This is the last message, but the cursor is not at the end of
+ ;; the buffer. Move it there.
+ (goto-char (point-max)))
+
(t
;; This is the last message - change the return value
(setq ret t)))
@@ -1601,6 +1543,39 @@ to show, nil otherwise."
(goto-char (point-max))))
r))
+(defun notmuch-show-next-matching-message ()
+ "Show the next matching message."
+ (interactive)
+ (let (r)
+ (while (and (setq r (notmuch-show-goto-message-next))
+ (not (notmuch-show-get-prop :match))))
+ (if r
+ (progn
+ (notmuch-show-mark-read)
+ (notmuch-show-message-adjust))
+ (goto-char (point-max)))))
+
+(defun notmuch-show-open-if-matched ()
+ "Open a message if it is matched (whether or not excluded)."
+ (let ((props (notmuch-show-get-message-properties)))
+ (notmuch-show-message-visible props (plist-get props :match))))
+
+(defun notmuch-show-goto-first-wanted-message ()
+ "Move to the first open message and mark it read"
+ (goto-char (point-min))
+ (if (notmuch-show-message-visible-p)
+ (notmuch-show-mark-read)
+ (notmuch-show-next-open-message))
+ (when (eobp)
+ ;; There are no matched non-excluded messages so open all matched
+ ;; (necessarily excluded) messages and go to the first.
+ (notmuch-show-mapc 'notmuch-show-open-if-matched)
+ (force-window-update)
+ (goto-char (point-min))
+ (if (notmuch-show-message-visible-p)
+ (notmuch-show-mark-read)
+ (notmuch-show-next-open-message))))
+
(defun notmuch-show-previous-open-message ()
"Show the previous open message."
(interactive)
@@ -1661,22 +1636,26 @@ TAG-CHANGES is a list of tag operations for `notmuch-tag'."
(let* ((current-tags (notmuch-show-get-tags))
(new-tags (notmuch-update-tags current-tags tag-changes)))
(unless (equal current-tags new-tags)
- (apply 'notmuch-tag (notmuch-show-get-message-id) tag-changes)
+ (funcall 'notmuch-tag (notmuch-show-get-message-id) tag-changes)
(notmuch-show-set-tags new-tags))))
-(defun notmuch-show-tag (&optional initial-input)
- "Change tags for the current message, read input from the minibuffer."
+(defun notmuch-show-tag (&optional tag-changes)
+ "Change tags for the current message.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES."
(interactive)
- (let ((tag-changes (notmuch-read-tag-changes
- initial-input (notmuch-show-get-message-id))))
- (apply 'notmuch-show-tag-message tag-changes)))
+ (setq tag-changes (funcall 'notmuch-tag (notmuch-show-get-message-id) tag-changes))
+ (let* ((current-tags (notmuch-show-get-tags))
+ (new-tags (notmuch-update-tags current-tags tag-changes)))
+ (unless (equal current-tags new-tags)
+ (notmuch-show-set-tags new-tags))))
-(defun notmuch-show-tag-all (&rest tag-changes)
- "Change tags for all messages in the current buffer.
+(defun notmuch-show-tag-all (&optional tag-changes)
+ "Change tags for all messages in the current show buffer.
-TAG-CHANGES is a list of tag operations for `notmuch-tag'."
- (interactive (notmuch-read-tag-changes nil notmuch-show-thread-id))
- (apply 'notmuch-tag (notmuch-show-get-messages-ids-search) tag-changes)
+See `notmuch-tag' for information on the format of TAG-CHANGES."
+ (interactive)
+ (setq tag-changes (funcall 'notmuch-tag (notmuch-show-get-messages-ids-search) tag-changes))
(notmuch-show-mapc
(lambda ()
(let* ((current-tags (notmuch-show-get-tags))
@@ -1748,13 +1727,10 @@ argument, hide all of the messages."
(defun notmuch-show-archive-thread (&optional unarchive)
"Archive each message in thread.
-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
-from the search from which this thread was originally shown.
+tag from each. If a prefix argument is given, the messages will
+be \"unarchived\" (ie. the \"inbox\" tag will be added instead of
+removed).
Note: This command is safe from any race condition of new messages
being delivered to the same thread. It does not archive the
@@ -1777,7 +1753,7 @@ buffer."
(notmuch-show-next-thread))
(defun notmuch-show-archive-message (&optional unarchive)
- "Archive the current message.
+ "Archive the current message (remove \"inbox\" tag).
If a prefix argument is given, the message will be
\"unarchived\" (ie. the \"inbox\" tag will be added instead of
@@ -1826,7 +1802,7 @@ thread from search."
(notmuch-common-do-stash (notmuch-show-get-from)))
(defun notmuch-show-stash-message-id ()
- "Copy message ID of current message to kill-ring."
+ "Copy id: query matching the current message to kill-ring."
(interactive)
(notmuch-common-do-stash (notmuch-show-get-message-id)))
diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el
new file mode 100644
index 00000000..0c0fc875
--- /dev/null
+++ b/emacs/notmuch-tag.el
@@ -0,0 +1,145 @@
+;; notmuch-tag.el --- tag messages within emacs
+;;
+;; Copyright © Carl Worth
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+
+(eval-when-compile (require 'cl))
+(require 'crm)
+(require 'notmuch-lib)
+
+(defcustom notmuch-before-tag-hook nil
+ "Hooks that are run before tags of a message are modified.
+
+'tags' will contain the tags that are about to be added or removed as
+a list of strings of the form \"+TAG\" or \"-TAG\".
+'query' will be a string containing the search query that determines
+the messages that are about to be tagged"
+
+ :type 'hook
+ :options '(notmuch-hl-line-mode)
+ :group 'notmuch-hooks)
+
+(defcustom notmuch-after-tag-hook nil
+ "Hooks that are run after tags of a message are modified.
+
+'tags' will contain the tags that were added or removed as
+a list of strings of the form \"+TAG\" or \"-TAG\".
+'query' will be a string containing the search query that determines
+the messages that were tagged"
+ :type 'hook
+ :options '(notmuch-hl-line-mode)
+ :group 'notmuch-hooks)
+
+(defvar notmuch-select-tag-history nil
+ "Variable to store minibuffer history for
+`notmuch-select-tag-with-completion' function.")
+
+(defvar notmuch-read-tag-changes-history nil
+ "Variable to store minibuffer history for
+`notmuch-read-tag-changes' function.")
+
+(defun notmuch-tag-completions (&optional search-terms)
+ (if (null search-terms)
+ (setq search-terms (list "*")))
+ (split-string
+ (with-output-to-string
+ (with-current-buffer standard-output
+ (apply 'call-process notmuch-command nil t
+ nil "search" "--output=tags" "--exclude=false" search-terms)))
+ "\n+" t))
+
+(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+ (let ((tag-list (notmuch-tag-completions search-terms)))
+ (completing-read prompt tag-list nil nil nil 'notmuch-select-tag-history)))
+
+(defun notmuch-read-tag-changes (&optional initial-input &rest search-terms)
+ (let* ((all-tag-list (notmuch-tag-completions))
+ (add-tag-list (mapcar (apply-partially 'concat "+") all-tag-list))
+ (remove-tag-list (mapcar (apply-partially 'concat "-")
+ (if (null search-terms)
+ all-tag-list
+ (notmuch-tag-completions search-terms))))
+ (tag-list (append add-tag-list remove-tag-list))
+ (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 "Tags (+add -drop): "
+ tag-list nil nil initial-input
+ 'notmuch-read-tag-changes-history))))
+
+(defun notmuch-update-tags (tags tag-changes)
+ "Return a copy of TAGS with additions and removals from TAG-CHANGES.
+
+TAG-CHANGES must be a list of tags names, each prefixed with
+either a \"+\" to indicate the tag should be added to TAGS if not
+present or a \"-\" to indicate that the tag should be removed
+from TAGS if present."
+ (let ((result-tags (copy-sequence tags)))
+ (dolist (tag-change tag-changes)
+ (let ((op (string-to-char tag-change))
+ (tag (unless (string= tag-change "") (substring tag-change 1))))
+ (case op
+ (?+ (unless (member tag result-tags)
+ (push tag result-tags)))
+ (?- (setq result-tags (delete tag result-tags)))
+ (otherwise
+ (error "Changed tag must be of the form `+this_tag' or `-that_tag'")))))
+ (sort result-tags 'string<)))
+
+(defun notmuch-tag (query &optional tag-changes)
+ "Add/remove tags in TAG-CHANGES to messages matching QUERY.
+
+QUERY should be a string containing the search-terms.
+TAG-CHANGES can take multiple forms. If TAG-CHANGES is a list of
+strings of the form \"+tag\" or \"-tag\" then those are the tag
+changes applied. If TAG-CHANGES is a string then it is
+interpreted as a single tag change. If TAG-CHANGES is the string
+\"-\" or \"+\", or null, then the user is prompted to enter the
+tag changes.
+
+Note: Other code should always use this function alter tags of
+messages instead of running (notmuch-call-notmuch-process \"tag\" ..)
+directly, so that hooks specified in notmuch-before-tag-hook and
+notmuch-after-tag-hook will be run."
+ ;; Perform some validation
+ (if (string-or-null-p tag-changes)
+ (if (or (string= tag-changes "-") (string= tag-changes "+") (null tag-changes))
+ (setq tag-changes (notmuch-read-tag-changes tag-changes query))
+ (setq tag-changes (list tag-changes))))
+ (mapc (lambda (tag-change)
+ (unless (string-match-p "^[-+]\\S-+$" tag-change)
+ (error "Tag must be of the form `+this_tag' or `-that_tag'")))
+ tag-changes)
+ (unless (null tag-changes)
+ (run-hooks 'notmuch-before-tag-hook)
+ (apply 'notmuch-call-notmuch-process "tag"
+ (append tag-changes (list "--" query)))
+ (run-hooks 'notmuch-after-tag-hook))
+ ;; in all cases we return tag-changes as a list
+ tag-changes)
+
+;;
+
+(provide 'notmuch-tag)
diff --git a/emacs/notmuch.el b/emacs/notmuch.el
index f851c6f7..c6236db2 100644
--- a/emacs/notmuch.el
+++ b/emacs/notmuch.el
@@ -48,11 +48,11 @@
;; required, but is available from http://notmuchmail.org).
(eval-when-compile (require 'cl))
-(require 'crm)
(require 'mm-view)
(require 'message)
(require 'notmuch-lib)
+(require 'notmuch-tag)
(require 'notmuch-show)
(require 'notmuch-mua)
(require 'notmuch-hello)
@@ -76,66 +76,6 @@ For example:
(defvar notmuch-query-history nil
"Variable to store minibuffer history for notmuch queries")
-(defvar notmuch-select-tag-history nil
- "Variable to store minibuffer history for
-`notmuch-select-tag-with-completion' function.")
-
-(defvar notmuch-read-tag-changes-history nil
- "Variable to store minibuffer history for
-`notmuch-read-tag-changes' function.")
-
-(defun notmuch-tag-completions (&optional search-terms)
- (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))
-
-(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
- (let ((tag-list (notmuch-tag-completions search-terms)))
- (completing-read prompt tag-list nil nil nil 'notmuch-select-tag-history)))
-
-(defun notmuch-read-tag-changes (&optional initial-input &rest search-terms)
- (let* ((all-tag-list (notmuch-tag-completions))
- (add-tag-list (mapcar (apply-partially 'concat "+") all-tag-list))
- (remove-tag-list (mapcar (apply-partially 'concat "-")
- (if (null search-terms)
- all-tag-list
- (notmuch-tag-completions search-terms))))
- (tag-list (append add-tag-list remove-tag-list))
- (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 "Tags (+add -drop): "
- tag-list nil nil initial-input
- 'notmuch-read-tag-changes-history))))
-
-(defun notmuch-update-tags (tags tag-changes)
- "Return a copy of TAGS with additions and removals from TAG-CHANGES.
-
-TAG-CHANGES must be a list of tags names, each prefixed with
-either a \"+\" to indicate the tag should be added to TAGS if not
-present or a \"-\" to indicate that the tag should be removed
-from TAGS if present."
- (let ((result-tags (copy-sequence tags)))
- (dolist (tag-change tag-changes)
- (let ((op (string-to-char tag-change))
- (tag (unless (string= tag-change "") (substring tag-change 1))))
- (case op
- (?+ (unless (member tag result-tags)
- (push tag result-tags)))
- (?- (setq result-tags (delete tag result-tags)))
- (otherwise
- (error "Changed tag must be of the form `+this_tag' or `-that_tag'")))))
- (sort result-tags 'string<)))
-
(defun notmuch-foreach-mime-part (function mm-handle)
(cond ((stringp (car mm-handle))
(dolist (part (cdr mm-handle))
@@ -505,7 +445,7 @@ Complete list of currently available key bindings:
"Display the currently selected thread."
(interactive)
(let ((thread-id (notmuch-search-find-thread-id))
- (subject (notmuch-prettify-subject (notmuch-search-find-subject))))
+ (subject (notmuch-search-find-subject)))
(if (> (length thread-id) 0)
(notmuch-show thread-id
(current-buffer)
@@ -543,51 +483,6 @@ and will also appear in a buffer named \"*Notmuch errors*\"."
(error (buffer-substring beg end))
))))))
-(defun notmuch-tag (query &rest tag-changes)
- "Add/remove tags in TAG-CHANGES to messages matching QUERY.
-
-TAG-CHANGES should be a list of strings of the form \"+tag\" or
-\"-tag\" and QUERY should be a string containing the
-search-query.
-
-Note: Other code should always use this function alter tags of
-messages instead of running (notmuch-call-notmuch-process \"tag\" ..)
-directly, so that hooks specified in notmuch-before-tag-hook and
-notmuch-after-tag-hook will be run."
- ;; Perform some validation
- (mapc (lambda (tag-change)
- (unless (string-match-p "^[-+]\\S-+$" tag-change)
- (error "Tag must be of the form `+this_tag' or `-that_tag'")))
- tag-changes)
- (unless (null tag-changes)
- (run-hooks 'notmuch-before-tag-hook)
- (apply 'notmuch-call-notmuch-process "tag"
- (append tag-changes (list "--" query)))
- (run-hooks 'notmuch-after-tag-hook)))
-
-(defcustom notmuch-before-tag-hook nil
- "Hooks that are run before tags of a message are modified.
-
-'tags' will contain the tags that are about to be added or removed as
-a list of strings of the form \"+TAG\" or \"-TAG\".
-'query' will be a string containing the search query that determines
-the messages that are about to be tagged"
-
- :type 'hook
- :options '(notmuch-hl-line-mode)
- :group 'notmuch-hooks)
-
-(defcustom notmuch-after-tag-hook nil
- "Hooks that are run after tags of a message are modified.
-
-'tags' will contain the tags that were added or removed as
-a list of strings of the form \"+TAG\" or \"-TAG\".
-'query' will be a string containing the search query that determines
-the messages that were tagged"
- :type 'hook
- :options '(notmuch-hl-line-mode)
- :group 'notmuch-hooks)
-
(defun notmuch-search-set-tags (tags)
(save-excursion
(end-of-line)
@@ -622,19 +517,10 @@ the messages that were tagged"
(forward-line 1))
output)))
-(defun notmuch-search-tag-thread (&rest tag-changes)
- "Change tags for the currently selected thread.
-
-See `notmuch-search-tag-region' for details."
- (apply 'notmuch-search-tag-region (point) (point) tag-changes))
-
-(defun notmuch-search-tag-region (beg end &rest tag-changes)
- "Change tags for threads in the given region.
-
-TAGS is a list of tag operations for `notmuch-tag'. The tags are
-added or removed for all threads in the region from BEG to END."
+(defun notmuch-search-tag-region (beg end &optional tag-changes)
+ "Change tags for threads in the given region."
(let ((search-string (notmuch-search-find-thread-id-region-search beg end)))
- (apply 'notmuch-tag search-string tag-changes)
+ (setq tag-changes (funcall 'notmuch-tag search-string tag-changes))
(save-excursion
(let ((last-line (line-number-at-pos end))
(max-line (- (line-number-at-pos (point-max)) 2)))
@@ -644,14 +530,14 @@ added or removed for all threads in the region from BEG to END."
(notmuch-update-tags (notmuch-search-get-tags) tag-changes))
(forward-line))))))
-(defun notmuch-search-tag (&optional initial-input)
- "Change tags for the currently selected thread or region."
+(defun notmuch-search-tag (&optional tag-changes)
+ "Change tags for the currently selected thread or region.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES."
(interactive)
(let* ((beg (if (region-active-p) (region-beginning) (point)))
- (end (if (region-active-p) (region-end) (point)))
- (search-string (notmuch-search-find-thread-id-region-search beg end))
- (tags (notmuch-read-tag-changes initial-input search-string)))
- (apply 'notmuch-search-tag-region beg end tags)))
+ (end (if (region-active-p) (region-end) (point))))
+ (funcall 'notmuch-search-tag-region beg end tag-changes)))
(defun notmuch-search-add-tag ()
"Same as `notmuch-search-tag' but sets initial input to '+'."
@@ -668,7 +554,7 @@ added or removed for all threads in the region from BEG to END."
This function advances the next thread when finished."
(interactive)
- (notmuch-search-tag-thread "-inbox")
+ (notmuch-search-tag '("-inbox"))
(notmuch-search-next-thread))
(defvar notmuch-search-process-filter-data nil
@@ -712,12 +598,12 @@ This function advances the next thread when finished."
Here is an example of how to color search results based on tags.
(the following text would be placed in your ~/.emacs file):
- (setq notmuch-search-line-faces '((\"delete\" . (:foreground \"red\"
+ (setq notmuch-search-line-faces '((\"deleted\" . (:foreground \"red\"
:background \"blue\"))
(\"unread\" . (:foreground \"green\"))))
The attributes defined for matching tags are merged, with later
-attributes overriding earlier. A message having both \"delete\"
+attributes overriding earlier. A message having both \"deleted\"
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))
@@ -872,16 +758,17 @@ non-authors is found, assume that all of the authors match."
(goto-char (point-max))
(if (/= (match-beginning 1) line)
(insert (concat "Error: Unexpected output from notmuch search:\n" (substring string line (match-beginning 1)) "\n")))
- (let ((beg (point)))
- (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)
- (when (string= thread-id notmuch-search-target-thread)
- (set 'found-target beg)
- (set 'notmuch-search-target-thread "found")))
+ ;; We currently just throw away excluded matches.
+ (unless (eq (aref count 1) ?0)
+ (let ((beg (point)))
+ (notmuch-search-show-result date count authors subject tags)
+ (notmuch-search-color-line beg (point) tag-list)
+ (put-text-property beg (point) 'notmuch-search-thread-id thread-id)
+ (put-text-property beg (point) 'notmuch-search-authors authors)
+ (put-text-property beg (point) 'notmuch-search-subject subject)
+ (when (string= thread-id notmuch-search-target-thread)
+ (set 'found-target beg)
+ (set 'notmuch-search-target-thread "found"))))
(set 'line (match-end 0)))
(set 'more nil)
(while (and (< line (length string)) (= (elt string line) ?\n))
@@ -893,18 +780,11 @@ non-authors is found, assume that all of the authors match."
(goto-char found-target)))
(delete-process proc))))
-(defun notmuch-search-tag-all (&rest tag-changes)
- "Add/remove tags from all matching messages.
-
-This command adds or removes tags from all messages matching the
-current search terms. When called interactively, this command
-will prompt for tags to be added or removed. Tags prefixed with
-'+' will be added and tags prefixed with '-' will be removed.
+(defun notmuch-search-tag-all (&optional tag-changes)
+ "Add/remove tags from all messages in current search buffer.
-Each character of the tag name may consist of alphanumeric
-characters as well as `_.+-'.
-"
- (interactive (notmuch-read-tag-changes))
+See `notmuch-tag' for information on the format of TAG-CHANGES."
+ (interactive)
(apply 'notmuch-tag notmuch-search-query-string tag-changes))
(defun notmuch-search-buffer-title (query)
@@ -960,7 +840,7 @@ PROMPT is the string to prompt with."
completions)))
(t (list string)))))))
;; this was simpler than convincing completing-read to accept spaces:
- (define-key keymap (kbd "<tab>") 'minibuffer-complete)
+ (define-key keymap (kbd "TAB") 'minibuffer-complete)
(let ((history-delete-duplicates t))
(read-from-minibuffer prompt nil keymap nil
'notmuch-search-history nil nil)))))
diff --git a/lib/Makefile.local b/lib/Makefile.local
index 54c4dea4..8a9aa28a 100644
--- a/lib/Makefile.local
+++ b/lib/Makefile.local
@@ -5,7 +5,7 @@
# the library interface, (such as the deletion of an API or a major
# semantic change that breaks formerly functioning code).
#
-LIBNOTMUCH_VERSION_MAJOR = 2
+LIBNOTMUCH_VERSION_MAJOR = 3
# The minor version of the library interface. This should be incremented at
# the time of release for any additions to the library interface,
diff --git a/lib/database.cc b/lib/database.cc
index 16c4354f..761dc1a2 100644
--- a/lib/database.cc
+++ b/lib/database.cc
@@ -520,9 +520,10 @@ parse_references (void *ctx,
}
}
-notmuch_database_t *
-notmuch_database_create (const char *path)
+notmuch_status_t
+notmuch_database_create (const char *path, notmuch_database_t **database)
{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
notmuch_database_t *notmuch = NULL;
char *notmuch_path = NULL;
struct stat st;
@@ -530,6 +531,7 @@ notmuch_database_create (const char *path)
if (path == NULL) {
fprintf (stderr, "Error: Cannot create a database for a NULL path.\n");
+ status = NOTMUCH_STATUS_NULL_POINTER;
goto DONE;
}
@@ -537,12 +539,14 @@ notmuch_database_create (const char *path)
if (err) {
fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
path, strerror (errno));
+ status = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
if (! S_ISDIR (st.st_mode)) {
fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
path);
+ status = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
@@ -553,18 +557,30 @@ notmuch_database_create (const char *path)
if (err) {
fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
notmuch_path, strerror (errno));
+ status = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
- notmuch = notmuch_database_open (path,
- NOTMUCH_DATABASE_MODE_READ_WRITE);
- notmuch_database_upgrade (notmuch, NULL, NULL);
+ status = notmuch_database_open (path,
+ NOTMUCH_DATABASE_MODE_READ_WRITE,
+ &notmuch);
+ if (status)
+ goto DONE;
+ status = notmuch_database_upgrade (notmuch, NULL, NULL);
+ if (status) {
+ notmuch_database_close(notmuch);
+ notmuch = NULL;
+ }
DONE:
if (notmuch_path)
talloc_free (notmuch_path);
- return notmuch;
+ if (database)
+ *database = notmuch;
+ else
+ talloc_free (notmuch);
+ return status;
}
notmuch_status_t
@@ -578,10 +594,12 @@ _notmuch_database_ensure_writable (notmuch_database_t *notmuch)
return NOTMUCH_STATUS_SUCCESS;
}
-notmuch_database_t *
+notmuch_status_t
notmuch_database_open (const char *path,
- notmuch_database_mode_t mode)
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database)
{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
void *local = talloc_new (NULL);
notmuch_database_t *notmuch = NULL;
char *notmuch_path, *xapian_path;
@@ -590,8 +608,15 @@ notmuch_database_open (const char *path,
unsigned int i, version;
static int initialized = 0;
+ if (path == NULL) {
+ fprintf (stderr, "Error: Cannot open a database for a NULL path.\n");
+ status = NOTMUCH_STATUS_NULL_POINTER;
+ goto DONE;
+ }
+
if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
fprintf (stderr, "Out of memory\n");
+ status = NOTMUCH_STATUS_OUT_OF_MEMORY;
goto DONE;
}
@@ -599,11 +624,13 @@ notmuch_database_open (const char *path,
if (err) {
fprintf (stderr, "Error opening database at %s: %s\n",
notmuch_path, strerror (errno));
+ status = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
fprintf (stderr, "Out of memory\n");
+ status = NOTMUCH_STATUS_OUT_OF_MEMORY;
goto DONE;
}
@@ -642,8 +669,9 @@ notmuch_database_open (const char *path,
" read-write mode.\n",
notmuch_path, version, NOTMUCH_DATABASE_VERSION);
notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
notmuch = NULL;
+ status = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
@@ -702,14 +730,19 @@ 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_database_destroy (notmuch);
notmuch = NULL;
+ status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
}
DONE:
talloc_free (local);
- return notmuch;
+ if (database)
+ *database = notmuch;
+ else
+ talloc_free (notmuch);
+ return status;
}
void
@@ -738,9 +771,19 @@ notmuch_database_close (notmuch_database_t *notmuch)
}
delete notmuch->term_gen;
+ notmuch->term_gen = NULL;
delete notmuch->query_parser;
+ notmuch->query_parser = NULL;
delete notmuch->xapian_db;
+ notmuch->xapian_db = NULL;
delete notmuch->value_range_processor;
+ notmuch->value_range_processor = NULL;
+}
+
+void
+notmuch_database_destroy (notmuch_database_t *notmuch)
+{
+ notmuch_database_close (notmuch);
talloc_free (notmuch);
}
@@ -912,8 +955,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
mtime = Xapian::sortable_unserialise (
document.get_value (NOTMUCH_VALUE_TIMESTAMP));
- directory = notmuch_database_get_directory (notmuch,
- term.c_str() + 10);
+ directory = _notmuch_directory_create (notmuch, term.c_str() + 10,
+ NOTMUCH_FIND_CREATE, &status);
notmuch_directory_set_mtime (directory, mtime);
notmuch_directory_destroy (directory);
}
@@ -1154,9 +1197,17 @@ _notmuch_database_split_path (void *ctx,
return NOTMUCH_STATUS_SUCCESS;
}
+/* Find the document ID of the specified directory.
+ *
+ * If (flags & NOTMUCH_FIND_CREATE), a new directory document will be
+ * created if one does not exist for 'path'. Otherwise, if the
+ * directory document does not exist, this sets *directory_id to
+ * ((unsigned int)-1) and returns NOTMUCH_STATUS_SUCCESS.
+ */
notmuch_status_t
_notmuch_database_find_directory_id (notmuch_database_t *notmuch,
const char *path,
+ notmuch_find_flags_t flags,
unsigned int *directory_id)
{
notmuch_directory_t *directory;
@@ -1167,8 +1218,8 @@ _notmuch_database_find_directory_id (notmuch_database_t *notmuch,
return NOTMUCH_STATUS_SUCCESS;
}
- directory = _notmuch_directory_create (notmuch, path, &status);
- if (status) {
+ directory = _notmuch_directory_create (notmuch, path, flags, &status);
+ if (status || !directory) {
*directory_id = -1;
return status;
}
@@ -1197,13 +1248,16 @@ _notmuch_database_get_directory_path (void *ctx,
* database path), return a new string (with 'ctx' as the talloc
* owner) suitable for use as a direntry term value.
*
- * The necessary directory documents will be created in the database
- * as needed.
+ * If (flags & NOTMUCH_FIND_CREATE), the necessary directory documents
+ * will be created in the database as needed. Otherwise, if the
+ * necessary directory documents do not exist, this sets
+ * *direntry to NULL and returns NOTMUCH_STATUS_SUCCESS.
*/
notmuch_status_t
_notmuch_database_filename_to_direntry (void *ctx,
notmuch_database_t *notmuch,
const char *filename,
+ notmuch_find_flags_t flags,
char **direntry)
{
const char *relative, *directory, *basename;
@@ -1217,10 +1271,12 @@ _notmuch_database_filename_to_direntry (void *ctx,
if (status)
return status;
- status = _notmuch_database_find_directory_id (notmuch, directory,
+ status = _notmuch_database_find_directory_id (notmuch, directory, flags,
&directory_id);
- if (status)
+ if (status || directory_id == (unsigned int)-1) {
+ *direntry = NULL;
return status;
+ }
*direntry = talloc_asprintf (ctx, "%u:%s", directory_id, basename);
@@ -1261,20 +1317,27 @@ _notmuch_database_relative_path (notmuch_database_t *notmuch,
return relative;
}
-notmuch_directory_t *
+notmuch_status_t
notmuch_database_get_directory (notmuch_database_t *notmuch,
- const char *path)
+ const char *path,
+ notmuch_directory_t **directory)
{
notmuch_status_t status;
+ if (directory == NULL)
+ return NOTMUCH_STATUS_NULL_POINTER;
+ *directory = NULL;
+
try {
- return _notmuch_directory_create (notmuch, path, &status);
+ *directory = _notmuch_directory_create (notmuch, path,
+ NOTMUCH_FIND_LOOKUP, &status);
} catch (const Xapian::Error &error) {
fprintf (stderr, "A Xapian exception occurred getting directory: %s.\n",
error.get_msg().c_str());
notmuch->exception_reported = TRUE;
- return NULL;
+ status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
}
+ return status;
}
/* Allocate a document ID that satisfies the following criteria:
@@ -1831,9 +1894,9 @@ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
local = talloc_new (notmuch);
try {
- status = _notmuch_database_filename_to_direntry (local, notmuch,
- filename, &direntry);
- if (status)
+ status = _notmuch_database_filename_to_direntry (
+ local, notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
+ if (status || !direntry)
goto DONE;
term = talloc_asprintf (local, "%s%s", prefix, direntry);
diff --git a/lib/directory.cc b/lib/directory.cc
index 70e1693e..6a3ffed7 100644
--- a/lib/directory.cc
+++ b/lib/directory.cc
@@ -82,28 +82,41 @@ find_directory_document (notmuch_database_t *notmuch,
return NOTMUCH_PRIVATE_STATUS_SUCCESS;
}
+/* Find or create a directory document.
+ *
+ * 'path' should be a path relative to the path of 'database', or else
+ * should be an absolute path with initial components that match the
+ * path of 'database'.
+ *
+ * If (flags & NOTMUCH_FIND_CREATE), then the directory document will
+ * be created if it does not exist. Otherwise, if the directory
+ * document does not exist, *status_ret is set to
+ * NOTMUCH_STATUS_SUCCESS and this returns NULL.
+ */
notmuch_directory_t *
_notmuch_directory_create (notmuch_database_t *notmuch,
const char *path,
+ notmuch_find_flags_t flags,
notmuch_status_t *status_ret)
{
Xapian::WritableDatabase *db;
notmuch_directory_t *directory;
notmuch_private_status_t private_status;
const char *db_path;
+ notmuch_bool_t create = (flags & NOTMUCH_FIND_CREATE);
*status_ret = NOTMUCH_STATUS_SUCCESS;
path = _notmuch_database_relative_path (notmuch, path);
- if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
+ if (create && notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
INTERNAL_ERROR ("Failure to ensure database is writable");
- db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-
directory = talloc (notmuch, notmuch_directory_t);
- if (unlikely (directory == NULL))
+ if (unlikely (directory == NULL)) {
+ *status_ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
return NULL;
+ }
directory->notmuch = notmuch;
@@ -122,6 +135,13 @@ _notmuch_directory_create (notmuch_database_t *notmuch,
directory->document_id = directory->doc.get_docid ();
if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+ if (!create) {
+ notmuch_directory_destroy (directory);
+ directory = NULL;
+ *status_ret = NOTMUCH_STATUS_SUCCESS;
+ goto DONE;
+ }
+
void *local = talloc_new (directory);
const char *parent, *basename;
Xapian::docid parent_id;
@@ -133,7 +153,13 @@ _notmuch_directory_create (notmuch_database_t *notmuch,
_notmuch_database_split_path (local, path, &parent, &basename);
- _notmuch_database_find_directory_id (notmuch, parent, &parent_id);
+ *status_ret = _notmuch_database_find_directory_id (
+ notmuch, parent, NOTMUCH_FIND_CREATE, &parent_id);
+ if (*status_ret) {
+ notmuch_directory_destroy (directory);
+ directory = NULL;
+ goto DONE;
+ }
if (basename) {
term = talloc_asprintf (local, "%s%u:%s",
@@ -145,6 +171,8 @@ _notmuch_directory_create (notmuch_database_t *notmuch,
directory->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
Xapian::sortable_serialise (0));
+ db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
directory->document_id = _notmuch_database_generate_doc_id (notmuch);
db->replace_document (directory->document_id, directory->doc);
talloc_free (local);
@@ -158,10 +186,11 @@ _notmuch_directory_create (notmuch_database_t *notmuch,
error.get_msg().c_str());
notmuch->exception_reported = TRUE;
notmuch_directory_destroy (directory);
+ directory = NULL;
*status_ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
- return NULL;
}
+ DONE:
if (db_path != path)
free ((char *) db_path);
diff --git a/lib/message.cc b/lib/message.cc
index 00754254..67875065 100644
--- a/lib/message.cc
+++ b/lib/message.cc
@@ -495,9 +495,8 @@ _notmuch_message_add_filename (notmuch_message_t *message,
if (status)
return status;
- status = _notmuch_database_filename_to_direntry (local,
- message->notmuch,
- filename, &direntry);
+ status = _notmuch_database_filename_to_direntry (
+ local, message->notmuch, filename, NOTMUCH_FIND_CREATE, &direntry);
if (status)
return status;
@@ -541,9 +540,9 @@ _notmuch_message_remove_filename (notmuch_message_t *message,
notmuch_status_t status;
Xapian::TermIterator i, last;
- status = _notmuch_database_filename_to_direntry (local, message->notmuch,
- filename, &direntry);
- if (status)
+ status = _notmuch_database_filename_to_direntry (
+ local, message->notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
+ if (status || !direntry)
return status;
/* Unlink this file from its parent directory. */
diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h
index 7bf153e0..bfb41116 100644
--- a/lib/notmuch-private.h
+++ b/lib/notmuch-private.h
@@ -146,8 +146,20 @@ typedef enum _notmuch_private_status {
: \
(notmuch_status_t) private_status)
+/* Flags shared by various lookup functions. */
+typedef enum _notmuch_find_flags {
+ /* Lookup without creating any documents. This is the default
+ * behavior. */
+ NOTMUCH_FIND_LOOKUP = 0,
+ /* If set, create the necessary document (or documents) if they
+ * are missing. Requires a read/write database. */
+ NOTMUCH_FIND_CREATE = 1<<0,
+} notmuch_find_flags_t;
+
typedef struct _notmuch_doc_id_set notmuch_doc_id_set_t;
+typedef struct _notmuch_string_list notmuch_string_list_t;
+
/* database.cc */
/* Lookup a prefix value by name.
@@ -186,6 +198,7 @@ _notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
notmuch_status_t
_notmuch_database_find_directory_id (notmuch_database_t *database,
const char *path,
+ notmuch_find_flags_t flags,
unsigned int *directory_id);
const char *
@@ -197,6 +210,7 @@ notmuch_status_t
_notmuch_database_filename_to_direntry (void *ctx,
notmuch_database_t *notmuch,
const char *filename,
+ notmuch_find_flags_t flags,
char **direntry);
/* directory.cc */
@@ -204,6 +218,7 @@ _notmuch_database_filename_to_direntry (void *ctx,
notmuch_directory_t *
_notmuch_directory_create (notmuch_database_t *notmuch,
const char *path,
+ notmuch_find_flags_t flags,
notmuch_status_t *status_ret);
unsigned int
@@ -216,6 +231,7 @@ _notmuch_thread_create (void *ctx,
notmuch_database_t *notmuch,
unsigned int seed_doc_id,
notmuch_doc_id_set_t *match_set,
+ notmuch_string_list_t *excluded_terms,
notmuch_sort_t sort);
/* message.cc */
@@ -401,6 +417,7 @@ typedef struct _notmuch_message_list {
*/
struct visible _notmuch_messages {
notmuch_bool_t is_of_list_type;
+ notmuch_doc_id_set_t *excluded_doc_ids;
notmuch_message_node_t *iterator;
};
@@ -458,11 +475,11 @@ typedef struct _notmuch_string_node {
struct _notmuch_string_node *next;
} notmuch_string_node_t;
-typedef struct visible _notmuch_string_list {
+struct visible _notmuch_string_list {
int length;
notmuch_string_node_t *head;
notmuch_string_node_t **tail;
-} notmuch_string_list_t;
+};
notmuch_string_list_t *
_notmuch_string_list_create (const void *ctx);
@@ -491,8 +508,26 @@ notmuch_filenames_t *
_notmuch_filenames_create (const void *ctx,
notmuch_string_list_t *list);
-#pragma GCC visibility pop
-
NOTMUCH_END_DECLS
+#ifdef __cplusplus
+/* Implicit typecast from 'void *' to 'T *' is okay in C, but not in
+ * C++. In talloc_steal, an explicit cast is provided for type safety
+ * in some GCC versions. Otherwise, a cast is required. Provide a
+ * template function for this to maintain type safety, and redefine
+ * talloc_steal to use it.
+ */
+#if !(__GNUC__ >= 3)
+template <class T> T *
+_notmuch_talloc_steal (const void *new_ctx, const T *ptr)
+{
+ return static_cast<T *> (talloc_steal (new_ctx, ptr));
+}
+#undef talloc_steal
+#define talloc_steal _notmuch_talloc_steal
+#endif
+#endif
+
+#pragma GCC visibility pop
+
#endif
diff --git a/lib/notmuch.h b/lib/notmuch.h
index 7929fe72..3633bedd 100644
--- a/lib/notmuch.h
+++ b/lib/notmuch.h
@@ -133,27 +133,38 @@ typedef struct _notmuch_filenames notmuch_filenames_t;
*
* After a successful call to notmuch_database_create, the returned
* database will be open so the caller should call
- * notmuch_database_close when finished with it.
+ * notmuch_database_destroy when finished with it.
*
* The database will not yet have any data in it
* (notmuch_database_create itself is a very cheap function). Messages
* contained within 'path' can be added to the database by calling
* notmuch_database_add_message.
*
- * In case of any failure, this function returns NULL, (after printing
- * an error message on stderr).
+ * In case of any failure, this function returns an error status and
+ * sets *database to NULL (after printing an error message on stderr).
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully created the database.
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The given 'path' argument is NULL.
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to create the
+ * database file (such as permission denied, or file not found,
+ * etc.), or the database already exists.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
*/
-notmuch_database_t *
-notmuch_database_create (const char *path);
+notmuch_status_t
+notmuch_database_create (const char *path, notmuch_database_t **database);
typedef enum {
NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
NOTMUCH_DATABASE_MODE_READ_WRITE
} notmuch_database_mode_t;
-/* XXX: I think I'd like this to take an extra argument of
- * notmuch_status_t* for returning a status value on failure. */
-
/* Open an existing notmuch database located at 'path'.
*
* The database should have been created at some time in the past,
@@ -165,21 +176,50 @@ typedef enum {
* An existing notmuch database can be identified by the presence of a
* directory named ".notmuch" below 'path'.
*
- * The caller should call notmuch_database_close when finished with
+ * The caller should call notmuch_database_destroy when finished with
* this database.
*
- * In case of any failure, this function returns NULL, (after printing
- * an error message on stderr).
+ * In case of any failure, this function returns an error status and
+ * sets *database to NULL (after printing an error message on stderr).
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully opened the database.
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The given 'path' argument is NULL.
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the
+ * database file (such as permission denied, or file not found,
+ * etc.), or the database version is unknown.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
*/
-notmuch_database_t *
+notmuch_status_t
notmuch_database_open (const char *path,
- notmuch_database_mode_t mode);
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database);
-/* Close the given notmuch database, freeing all associated
- * resources. See notmuch_database_open. */
+/* Close the given notmuch database.
+ *
+ * After notmuch_database_close has been called, calls to other
+ * functions on objects derived from this database may either behave
+ * as if the database had not been closed (e.g., if the required data
+ * has been cached) or may fail with a
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION.
+ *
+ * notmuch_database_close can be called multiple times. Later calls
+ * have no effect.
+ */
void
notmuch_database_close (notmuch_database_t *database);
+/* Destroy the notmuch database, closing it if necessary and freeing
+* all associated resources. */
+void
+notmuch_database_destroy (notmuch_database_t *database);
+
/* Return the database path of the given database.
*
* The return value is a string owned by notmuch so should not be
@@ -260,11 +300,22 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch);
* (see notmuch_database_get_path), or else should be an absolute path
* with initial components that match the path of 'database'.
*
- * Can return NULL if a Xapian exception occurs.
+ * If this directory object does not exist in the database, this
+ * returns NOTMUCH_STATUS_SUCCESS and sets *directory to NULL.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully retrieved directory.
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The given 'directory' argument is NULL.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred;
+ * directory not retrieved.
*/
-notmuch_directory_t *
+notmuch_status_t
notmuch_database_get_directory (notmuch_database_t *database,
- const char *path);
+ const char *path,
+ notmuch_directory_t **directory);
/* Add a new message to the given notmuch database or associate an
* additional filename with an existing message.
@@ -449,6 +500,26 @@ typedef enum {
const char *
notmuch_query_get_query_string (notmuch_query_t *query);
+/* Specify whether to omit excluded results or simply flag them. By
+ * default, this is set to TRUE.
+ *
+ * If this is TRUE, notmuch_query_search_messages will omit excluded
+ * messages from the results. notmuch_query_search_threads will omit
+ * threads that match only in excluded messages, but will include all
+ * messages in threads that match in at least one non-excluded
+ * message.
+ *
+ * The performance difference when calling
+ * notmuch_query_search_messages should be relatively small (and both
+ * should be very fast). However, in some cases,
+ * notmuch_query_search_threads is very much faster when omitting
+ * excluded messages as it does not need to construct the threads that
+ * only match in excluded messages.
+ */
+
+void
+notmuch_query_set_omit_excluded (notmuch_query_t *query, notmuch_bool_t omit_excluded);
+
/* Specify the sorting desired for this query. */
void
notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
@@ -665,8 +736,10 @@ notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
/* Get the number of messages in 'thread' that matched the search.
*
* This count includes only the messages in this thread that were
- * matched by the search from which the thread was created. Contrast
- * with notmuch_thread_get_total_messages() .
+ * matched by the search from which the thread was created and were
+ * not excluded by any exclude tags passed in with the query (see
+ * notmuch_query_add_tag_exclude). Contrast with
+ * notmuch_thread_get_total_messages() .
*/
int
notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
@@ -895,7 +968,8 @@ notmuch_message_get_filenames (notmuch_message_t *message);
/* Message flags */
typedef enum _notmuch_message_flag {
- NOTMUCH_MESSAGE_FLAG_MATCH
+ NOTMUCH_MESSAGE_FLAG_MATCH,
+ NOTMUCH_MESSAGE_FLAG_EXCLUDED
} notmuch_message_flag_t;
/* Get a value of a flag for the email corresponding to 'message'. */
diff --git a/lib/query.cc b/lib/query.cc
index 0b366025..e9c1a2d1 100644
--- a/lib/query.cc
+++ b/lib/query.cc
@@ -28,6 +28,7 @@ struct _notmuch_query {
const char *query_string;
notmuch_sort_t sort;
notmuch_string_list_t *exclude_terms;
+ notmuch_bool_t omit_excluded;
};
typedef struct _notmuch_mset_messages {
@@ -57,15 +58,27 @@ struct visible _notmuch_threads {
notmuch_doc_id_set_t match_set;
};
+/* We need this in the message functions so forward declare. */
+static notmuch_bool_t
+_notmuch_doc_id_set_init (void *ctx,
+ notmuch_doc_id_set_t *doc_ids,
+ GArray *arr);
+
+static notmuch_bool_t
+_debug_query (void)
+{
+ char *env = getenv ("NOTMUCH_DEBUG_QUERY");
+ return (env && strcmp (env, "") != 0);
+}
+
notmuch_query_t *
notmuch_query_create (notmuch_database_t *notmuch,
const char *query_string)
{
notmuch_query_t *query;
-#ifdef DEBUG_QUERY
- fprintf (stderr, "Query string is:\n%s\n", query_string);
-#endif
+ if (_debug_query ())
+ fprintf (stderr, "Query string is:\n%s\n", query_string);
query = talloc (NULL, notmuch_query_t);
if (unlikely (query == NULL))
@@ -79,6 +92,8 @@ notmuch_query_create (notmuch_database_t *notmuch,
query->exclude_terms = _notmuch_string_list_create (query);
+ query->omit_excluded = TRUE;
+
return query;
}
@@ -89,6 +104,12 @@ notmuch_query_get_query_string (notmuch_query_t *query)
}
void
+notmuch_query_set_omit_excluded (notmuch_query_t *query, notmuch_bool_t omit_excluded)
+{
+ query->omit_excluded = omit_excluded;
+}
+
+void
notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
{
query->sort = sort;
@@ -122,12 +143,16 @@ _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. */
+/* Return a query that matches messages with the excluded tags
+ * registered with query. Any tags that explicitly appear in xquery
+ * will not be excluded, and will be removed from the list of exclude
+ * tags. The caller of this function has to combine the returned
+ * query appropriately.*/
static Xapian::Query
_notmuch_exclude_tags (notmuch_query_t *query, Xapian::Query xquery)
{
+ Xapian::Query exclude_query = Xapian::Query::MatchNothing;
+
for (notmuch_string_node_t *term = query->exclude_terms->head; term;
term = term->next) {
Xapian::TermIterator it = xquery.get_terms_begin ();
@@ -137,10 +162,12 @@ _notmuch_exclude_tags (notmuch_query_t *query, Xapian::Query xquery)
break;
}
if (it == end)
- xquery = Xapian::Query (Xapian::Query::OP_AND_NOT,
- xquery, Xapian::Query (term->string));
+ exclude_query = Xapian::Query (Xapian::Query::OP_OR,
+ exclude_query, Xapian::Query (term->string));
+ else
+ term->string = talloc_strdup (query, "");
}
- return xquery;
+ return exclude_query;
}
notmuch_messages_t *
@@ -168,8 +195,9 @@ notmuch_query_search_messages (notmuch_query_t *query)
Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
_find_prefix ("type"),
"mail"));
- Xapian::Query string_query, final_query;
+ Xapian::Query string_query, final_query, exclude_query;
Xapian::MSet mset;
+ Xapian::MSetIterator iterator;
unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
Xapian::QueryParser::FLAG_PHRASE |
Xapian::QueryParser::FLAG_LOVEHATE |
@@ -187,8 +215,36 @@ notmuch_query_search_messages (notmuch_query_t *query)
final_query = Xapian::Query (Xapian::Query::OP_AND,
mail_query, string_query);
}
+ messages->base.excluded_doc_ids = NULL;
+
+ if (query->exclude_terms) {
+ exclude_query = _notmuch_exclude_tags (query, final_query);
+
+ if (query->omit_excluded)
+ final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
+ final_query, exclude_query);
+ else {
+ exclude_query = Xapian::Query (Xapian::Query::OP_AND,
+ exclude_query, final_query);
+
+ enquire.set_weighting_scheme (Xapian::BoolWeight());
+ enquire.set_query (exclude_query);
+
+ mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
+
+ GArray *excluded_doc_ids = g_array_new (FALSE, FALSE, sizeof (unsigned int));
+
+ for (iterator = mset.begin (); iterator != mset.end (); iterator++) {
+ unsigned int doc_id = *iterator;
+ g_array_append_val (excluded_doc_ids, doc_id);
+ }
+ messages->base.excluded_doc_ids = talloc (messages, _notmuch_doc_id_set);
+ _notmuch_doc_id_set_init (query, messages->base.excluded_doc_ids,
+ excluded_doc_ids);
+ g_array_unref (excluded_doc_ids);
+ }
+ }
- final_query = _notmuch_exclude_tags (query, final_query);
enquire.set_weighting_scheme (Xapian::BoolWeight());
@@ -206,9 +262,12 @@ notmuch_query_search_messages (notmuch_query_t *query)
break;
}
-#if DEBUG_QUERY
- fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
-#endif
+ if (_debug_query ()) {
+ fprintf (stderr, "Exclude query is:\n%s\n",
+ exclude_query.get_description ().c_str ());
+ fprintf (stderr, "Final query is:\n%s\n",
+ final_query.get_description ().c_str ());
+ }
enquire.set_query (final_query);
@@ -277,6 +336,10 @@ _notmuch_mset_messages_get (notmuch_messages_t *messages)
INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
}
+ if (messages->excluded_doc_ids &&
+ _notmuch_doc_id_set_contains (messages->excluded_doc_ids, doc_id))
+ notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, TRUE);
+
return message;
}
@@ -422,6 +485,7 @@ notmuch_threads_get (notmuch_threads_t *threads)
threads->query->notmuch,
doc_id,
&threads->match_set,
+ threads->query->exclude_terms,
threads->query->sort);
}
@@ -449,7 +513,7 @@ notmuch_query_count_messages (notmuch_query_t *query)
Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
_find_prefix ("type"),
"mail"));
- Xapian::Query string_query, final_query;
+ Xapian::Query string_query, final_query, exclude_query;
Xapian::MSet mset;
unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
Xapian::QueryParser::FLAG_PHRASE |
@@ -469,14 +533,20 @@ notmuch_query_count_messages (notmuch_query_t *query)
mail_query, string_query);
}
- final_query = _notmuch_exclude_tags (query, final_query);
+ exclude_query = _notmuch_exclude_tags (query, final_query);
+
+ final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
+ final_query, exclude_query);
enquire.set_weighting_scheme(Xapian::BoolWeight());
enquire.set_docid_order(Xapian::Enquire::ASCENDING);
-#if DEBUG_QUERY
- fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
-#endif
+ if (_debug_query ()) {
+ fprintf (stderr, "Exclude query is:\n%s\n",
+ exclude_query.get_description ().c_str ());
+ fprintf (stderr, "Final query is:\n%s\n",
+ final_query.get_description ().c_str ());
+ }
enquire.set_query (final_query);
diff --git a/lib/thread.cc b/lib/thread.cc
index 0435ee6d..e976d643 100644
--- a/lib/thread.cc
+++ b/lib/thread.cc
@@ -214,7 +214,8 @@ _thread_cleanup_author (notmuch_thread_t *thread,
*/
static void
_thread_add_message (notmuch_thread_t *thread,
- notmuch_message_t *message)
+ notmuch_message_t *message,
+ notmuch_string_list_t *exclude_terms)
{
notmuch_tags_t *tags;
const char *tag;
@@ -262,6 +263,15 @@ _thread_add_message (notmuch_thread_t *thread,
notmuch_tags_move_to_next (tags))
{
tag = notmuch_tags_get (tags);
+ /* Mark excluded messages. */
+ for (notmuch_string_node_t *term = exclude_terms->head; term;
+ term = term->next) {
+ /* We ignore initial 'K'. */
+ if (strcmp(tag, (term->string + 1)) == 0) {
+ notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, TRUE);
+ break;
+ }
+ }
g_hash_table_insert (thread->tags, xstrdup (tag), NULL);
}
}
@@ -321,7 +331,8 @@ _thread_add_matched_message (notmuch_thread_t *thread,
_thread_set_subject_from_message (thread, message);
}
- thread->matched_messages++;
+ if (!notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED))
+ thread->matched_messages++;
if (g_hash_table_lookup_extended (thread->message_hash,
notmuch_message_get_message_id (message), NULL,
@@ -392,6 +403,7 @@ _notmuch_thread_create (void *ctx,
notmuch_database_t *notmuch,
unsigned int seed_doc_id,
notmuch_doc_id_set_t *match_set,
+ notmuch_string_list_t *exclude_terms,
notmuch_sort_t sort)
{
notmuch_thread_t *thread;
@@ -467,7 +479,7 @@ _notmuch_thread_create (void *ctx,
if (doc_id == seed_doc_id)
message = seed_message;
- _thread_add_message (thread, message);
+ _thread_add_message (thread, message, exclude_terms);
if ( _notmuch_doc_id_set_contains (match_set, doc_id)) {
_notmuch_doc_id_set_remove (match_set, doc_id);
diff --git a/man/man1/notmuch-config.1 b/man/man1/notmuch-config.1
index a7468950..4f7985c3 100644
--- a/man/man1/notmuch-config.1
+++ b/man/man1/notmuch-config.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-CONFIG 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-CONFIG 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-config \- Access notmuch configuration file.
.SH SYNOPSIS
@@ -9,6 +9,8 @@ notmuch-config \- Access notmuch configuration file.
.B notmuch config set
.RI "<" section ">.<" item "> [" value " ...]"
+.B notmuch config list
+
.SH DESCRIPTION
The
@@ -35,6 +37,18 @@ If no values are provided, the specified configuration item will be
removed from the configuration file.
.RE
+.RS 4
+.TP 4
+.B list
+Every configuration item is printed to stdout, each on a separate line
+of the form:
+
+.RI "" section "." item "=" value
+
+No additional whitespace surrounds the dot or equals sign characters. In a
+multiple-value item (a list), the values are separated by semicolon characters.
+.RE
+
The available configuration items are described below.
.RS 4
@@ -85,6 +99,14 @@ directory hierarchy.
.RS 4
.TP 4
+.B search.exclude_tags
+A list of tags that will be excluded from search results by
+default. Using an excluded tag in a query will override that
+exclusion.
+.RE
+
+.RS 4
+.TP 4
.B maildir.synchronize_flags
If true, then the following maildir flags (in message filenames) will
be synchronized with the corresponding notmuch tags:
diff --git a/man/man1/notmuch-count.1 b/man/man1/notmuch-count.1
index 8de43453..8029174c 100644
--- a/man/man1/notmuch-count.1
+++ b/man/man1/notmuch-count.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-COUNT 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-COUNT 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-count \- Count messages matching the given search terms.
.SH SYNOPSIS
@@ -38,6 +38,14 @@ Output the number of matching messages. This is the default.
Output the number of matching threads.
.RE
.RE
+
+.RS 4
+.TP 4
+.BR \-\-exclude=(true|false)
+
+Specify whether to omit messages matching search.tag_exclude from the
+count (the default) or not.
+.RE
.RE
.RE
diff --git a/man/man1/notmuch-dump.1 b/man/man1/notmuch-dump.1
index f479e8bf..9c7dd84b 100644
--- a/man/man1/notmuch-dump.1
+++ b/man/man1/notmuch-dump.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-DUMP 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-DUMP 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-dump \- Creates a plain-text dump of the tags of each message.
diff --git a/man/man1/notmuch-new.1 b/man/man1/notmuch-new.1
index 613658d2..cd83a883 100644
--- a/man/man1/notmuch-new.1
+++ b/man/man1/notmuch-new.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-NEW 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-NEW 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-new \- Incorporate new mail into the notmuch database.
.SH SYNOPSIS
diff --git a/man/man1/notmuch-reply.1 b/man/man1/notmuch-reply.1
index bd95b5f8..fb5114ca 100644
--- a/man/man1/notmuch-reply.1
+++ b/man/man1/notmuch-reply.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-REPLY 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-REPLY 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-reply \- Constructs a reply template for a set of messages.
@@ -37,12 +37,17 @@ Supported options for
include
.RS
.TP 4
-.BR \-\-format= ( default | headers\-only )
+.BR \-\-format= ( default | json | headers\-only )
.RS
.TP 4
.BR default
Includes subject and quoted message body.
.TP
+.BR json
+Produces JSON output containing headers for a reply message and the
+contents of the original message. This output can be used by a client
+to create a reply message intelligently.
+.TP
.BR headers\-only
Only produces In\-Reply\-To, References, To, Cc, and Bcc headers.
.RE
@@ -63,6 +68,16 @@ values from the first that contains something other than only the
user's addresses.
.RE
.RE
+.RS
+.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
See \fBnotmuch-search-terms\fR(7)
for details of the supported syntax for <search-terms>.
@@ -73,7 +88,8 @@ 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.
+on issues found in multiple patches. The default format supports
+replying to multiple messages at once, but the JSON format does not.
.RE
.RE
diff --git a/man/man1/notmuch-restore.1 b/man/man1/notmuch-restore.1
index db0b697e..3156af76 100644
--- a/man/man1/notmuch-restore.1
+++ b/man/man1/notmuch-restore.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-RESTORE 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-RESTORE 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-restore \- Restores the tags from the given file (see notmuch dump).
diff --git a/man/man1/notmuch-search.1 b/man/man1/notmuch-search.1
index bf172207..5c72c4ba 100644
--- a/man/man1/notmuch-search.1
+++ b/man/man1/notmuch-search.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-SEARCH 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-SEARCH 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-search \- Search for messages matching the given search terms.
.SH SYNOPSIS
@@ -112,6 +112,19 @@ result from the end.
Limit the number of displayed results to N.
.RE
+.RS 4
+.TP 4
+.BR \-\-exclude=(true|false|flag)
+
+Specify whether to omit messages matching search.tag_exclude from the
+search results (the default) or not. The extra option
+.B flag
+only has an effect when
+.B --output=summary
+In this case all matching threads are returned but the "match count"
+is the number of matching non-excluded messages in the thread.
+.RE
+
.SH SEE ALSO
\fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
diff --git a/man/man1/notmuch-show.1 b/man/man1/notmuch-show.1
index d69834a1..efd30a02 100644
--- a/man/man1/notmuch-show.1
+++ b/man/man1/notmuch-show.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-SHOW 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-SHOW 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-show \- Show messages matching the given search terms.
.SH SYNOPSIS
@@ -84,12 +84,17 @@ http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html
.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 message or an attached message part, 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.
+performing any necessary MIME decoding. Note that messages with a
+simple body still have two parts: part 0 is the whole message and part
+1 is the body.
+
+For a multipart part, the part headers and body (including all child
+parts) is output.
The raw format must only be used with search terms matching single
message.
@@ -128,6 +133,25 @@ multipart/encrypted part will be replaced by the decrypted
content.
.RE
+.RS 4
+.TP 4
+.BR \-\-exclude=(true|false)
+
+Specify whether to omit threads only matching search.tag_exclude from
+the search results (the default) or not. In either case the excluded
+message will be marked with the exclude flag (except when output=mbox
+when there is nowhere to put the flag).
+
+If --entire-thread is specified then complete threads are returned
+regardless (with the excluded flag being set when appropriate) but
+threads that only match in an excluded message are not returned when
+.B --exclude=true.
+
+The default is
+.B --exclude=true.
+
+.RE
+
A common use of
.B notmuch show
is to display a single thread of email messages. For this, use a
diff --git a/man/man1/notmuch-tag.1 b/man/man1/notmuch-tag.1
index aa4546e4..27e682ef 100644
--- a/man/man1/notmuch-tag.1
+++ b/man/man1/notmuch-tag.1
@@ -1,4 +1,4 @@
-.TH NOTMUCH-TAG 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-TAG 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-tag \- Add/remove tags for all messages matching the search terms.
diff --git a/man/man1/notmuch.1 b/man/man1/notmuch.1
index 2afcc771..ebea4aa4 100644
--- a/man/man1/notmuch.1
+++ b/man/man1/notmuch.1
@@ -16,7 +16,7 @@
.\" along with this program. If not, see http://www.gnu.org/licenses/ .
.\"
.\" Author: Carl Worth <cworth@cworth.org>
-.TH NOTMUCH 1 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH 1 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch \- thread-based email index, search, and tagging
.SH SYNOPSIS
diff --git a/man/man5/notmuch-hooks.5 b/man/man5/notmuch-hooks.5
index 9662bdeb..b914a295 100644
--- a/man/man5/notmuch-hooks.5
+++ b/man/man5/notmuch-hooks.5
@@ -1,4 +1,4 @@
-.TH NOTMUCH-HOOKS 5 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-HOOKS 5 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-hooks \- hooks for notmuch
diff --git a/man/man7/notmuch-search-terms.7 b/man/man7/notmuch-search-terms.7
index 37ba9bb1..c559ed68 100644
--- a/man/man7/notmuch-search-terms.7
+++ b/man/man7/notmuch-search-terms.7
@@ -1,4 +1,4 @@
-.TH NOTMUCH-SEARCH-TERMS 7 2012-03-19 "Notmuch 0.12"
+.TH NOTMUCH-SEARCH-TERMS 7 2012-06-01 "Notmuch 0.13.2"
.SH NAME
notmuch-search-terms \- Syntax for notmuch queries
diff --git a/notmuch-client.h b/notmuch-client.h
index f4a62ccb..19b7f01f 100644
--- a/notmuch-client.h
+++ b/notmuch-client.h
@@ -62,43 +62,21 @@
#define STRINGIFY(s) STRINGIFY_(s)
#define STRINGIFY_(s) #s
-struct mime_node;
+typedef struct mime_node mime_node_t;
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,
- int indent);
- const char *header_start;
- void (*header) (const void *ctx,
- notmuch_message_t *message);
- void (*header_message_part) (GMimeMessage *message);
- const char *header_end;
- const char *body_start;
- void (*part_start) (GMimeObject *part,
- 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;
- const char *body_end;
- const char *message_end;
+ notmuch_status_t (*part) (const void *ctx,
+ struct mime_node *node, int indent,
+ const struct notmuch_show_params *params);
const char *message_set_sep;
const char *message_set_end;
} notmuch_show_format_t;
typedef struct notmuch_show_params {
notmuch_bool_t entire_thread;
+ notmuch_bool_t omit_excluded;
notmuch_bool_t raw;
int part;
#ifdef GMIME_ATLEAST_26
@@ -184,13 +162,22 @@ char *
query_string_from_args (void *ctx, int argc, char *argv[]);
notmuch_status_t
-show_message_body (notmuch_message_t *message,
- const notmuch_show_format_t *format,
- notmuch_show_params_t *params);
-
-notmuch_status_t
show_one_part (const char *filename, int part);
+void
+format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first);
+
+void
+format_headers_json (const void *ctx, GMimeMessage *message, notmuch_bool_t reply);
+
+typedef enum {
+ NOTMUCH_SHOW_TEXT_PART_REPLY = 1 << 0,
+} notmuch_show_text_part_flags;
+
+void
+show_text_part_content (GMimeObject *part, GMimeStream *stream_out,
+ notmuch_show_text_part_flags flags);
+
char *
json_quote_chararray (const void *ctx, const char *str, const size_t len);
@@ -288,7 +275,7 @@ debugger_is_active (void);
* parts. Message-type parts have one child, multipart-type parts
* have multiple children, and leaf parts have zero children.
*/
-typedef struct mime_node {
+struct mime_node {
/* The MIME object of this part. This will be a GMimeMessage,
* GMimePart, GMimeMultipart, or a subclass of one of these.
*
@@ -351,7 +338,7 @@ typedef struct mime_node {
* 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
diff --git a/notmuch-config.c b/notmuch-config.c
index 61fda3ea..3e37a2d6 100644
--- a/notmuch-config.c
+++ b/notmuch-config.c
@@ -377,7 +377,8 @@ notmuch_config_open (void *ctx,
if (notmuch_config_get_search_exclude_tags (config, &tmp) == NULL) {
if (is_new) {
- /* We do not set default search_exclude_tags for 0.12 */
+ const char *tags[] = { "deleted", "spam" };
+ notmuch_config_set_search_exclude_tags (config, tags, 2);
} else {
notmuch_config_set_search_exclude_tags (config, NULL, 0);
}
@@ -750,7 +751,7 @@ notmuch_config_command_get (void *ctx, char *item)
for (i = 0; i < length; i++)
printf ("%s\n", value[i]);
- free (value);
+ g_strfreev (value);
}
notmuch_config_close (config);
@@ -798,20 +799,78 @@ notmuch_config_command_set (void *ctx, char *item, int argc, char *argv[])
return ret;
}
+static int
+notmuch_config_command_list (void *ctx)
+{
+ notmuch_config_t *config;
+ char **groups;
+ size_t g, groups_length;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ groups = g_key_file_get_groups (config->key_file, &groups_length);
+ if (groups == NULL)
+ return 1;
+
+ for (g = 0; g < groups_length; g++) {
+ char **keys;
+ size_t k, keys_length;
+
+ keys = g_key_file_get_keys (config->key_file,
+ groups[g], &keys_length, NULL);
+ if (keys == NULL)
+ continue;
+
+ for (k = 0; k < keys_length; k++) {
+ char *value;
+
+ value = g_key_file_get_string (config->key_file,
+ groups[g], keys[k], NULL);
+ if (value != NULL) {
+ printf ("%s.%s=%s\n", groups[g], keys[k], value);
+ free (value);
+ }
+ }
+
+ g_strfreev (keys);
+ }
+
+ g_strfreev (groups);
+
+ notmuch_config_close (config);
+
+ return 0;
+}
+
int
notmuch_config_command (void *ctx, int argc, char *argv[])
{
argc--; argv++; /* skip subcommand argument */
- if (argc < 2) {
- fprintf (stderr, "Error: notmuch config requires at least two arguments.\n");
+ if (argc < 1) {
+ fprintf (stderr, "Error: notmuch config requires at least one argument.\n");
return 1;
}
- if (strcmp (argv[0], "get") == 0)
+ if (strcmp (argv[0], "get") == 0) {
+ if (argc != 2) {
+ fprintf (stderr, "Error: notmuch config get requires exactly "
+ "one argument.\n");
+ return 1;
+ }
return notmuch_config_command_get (ctx, argv[1]);
- else if (strcmp (argv[0], "set") == 0)
+ } else if (strcmp (argv[0], "set") == 0) {
+ if (argc < 2) {
+ fprintf (stderr, "Error: notmuch config set requires at least "
+ "one argument.\n");
+ return 1;
+ }
return notmuch_config_command_set (ctx, argv[1], argc - 2, argv + 2);
+ } else if (strcmp (argv[0], "list") == 0) {
+ return notmuch_config_command_list (ctx);
+ }
fprintf (stderr, "Unrecognized argument for notmuch config: %s\n",
argv[0]);
diff --git a/notmuch-count.c b/notmuch-count.c
index 63459fb6..2f981282 100644
--- a/notmuch-count.c
+++ b/notmuch-count.c
@@ -26,6 +26,12 @@ enum {
OUTPUT_MESSAGES,
};
+/* The following is to allow future options to be added more easily */
+enum {
+ EXCLUDE_TRUE,
+ EXCLUDE_FALSE,
+};
+
int
notmuch_count_command (void *ctx, int argc, char *argv[])
{
@@ -35,8 +41,7 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
char *query_str;
int opt_index;
int output = OUTPUT_MESSAGES;
- const char **search_exclude_tags;
- size_t search_exclude_tags_length;
+ int exclude = EXCLUDE_TRUE;
unsigned int i;
notmuch_opt_desc_t options[] = {
@@ -44,6 +49,10 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
(notmuch_keyword_t []){ { "threads", OUTPUT_THREADS },
{ "messages", OUTPUT_MESSAGES },
{ 0, 0 } } },
+ { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
+ (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
+ { "false", EXCLUDE_FALSE },
+ { 0, 0 } } },
{ 0, 0, 0, 0, 0 }
};
@@ -57,9 +66,8 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
if (config == NULL)
return 1;
- notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY);
- if (notmuch == NULL)
+ if (notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
return 1;
query_str = query_string_from_args (ctx, argc-opt_index, argv+opt_index);
@@ -78,10 +86,15 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
return 1;
}
- 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]);
+ if (exclude == EXCLUDE_TRUE) {
+ const char **search_exclude_tags;
+ size_t search_exclude_tags_length;
+
+ 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:
@@ -93,7 +106,7 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
}
notmuch_query_destroy (query);
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
return 0;
}
diff --git a/notmuch-dump.c b/notmuch-dump.c
index a7358756..37432142 100644
--- a/notmuch-dump.c
+++ b/notmuch-dump.c
@@ -36,9 +36,8 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
if (config == NULL)
return 1;
- notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY);
- if (notmuch == NULL)
+ if (notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
return 1;
char *output_file_name = NULL;
@@ -116,7 +115,7 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
fclose (output);
notmuch_query_destroy (query);
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
return 0;
}
diff --git a/notmuch-new.c b/notmuch-new.c
index 4f13535c..72dd558d 100644
--- a/notmuch-new.c
+++ b/notmuch-new.c
@@ -250,13 +250,13 @@ add_files_recursive (notmuch_database_t *notmuch,
notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
notmuch_message_t *message = NULL;
struct dirent **fs_entries = NULL;
- int i, num_fs_entries;
+ int i, num_fs_entries = 0;
notmuch_directory_t *directory;
notmuch_filenames_t *db_files = NULL;
notmuch_filenames_t *db_subdirs = NULL;
time_t stat_time;
struct stat st;
- notmuch_bool_t is_maildir, new_directory;
+ notmuch_bool_t is_maildir;
const char **tag;
if (stat (path, &st)) {
@@ -274,40 +274,27 @@ add_files_recursive (notmuch_database_t *notmuch,
fs_mtime = st.st_mtime;
- directory = notmuch_database_get_directory (notmuch, path);
- db_mtime = notmuch_directory_get_mtime (directory);
-
- new_directory = db_mtime ? FALSE : TRUE;
-
- /* XXX This is a temporary workaround. If we don't update the
- * database mtime until after processing messages in this
- * directory, then a 0 mtime is *not* sufficient to indicate that
- * this directory has no messages or subdirs in the database (for
- * example, if an earlier run skipped the mtime update because
- * fs_mtime == stat_time, or was interrupted before updating the
- * mtime at the end). To address this, we record a (bogus)
- * non-zero value before processing any child messages so that a
- * later run won't mistake this for a new directory (and, for
- * example, fail to detect removed files and subdirs).
- *
- * A better solution would be for notmuch_database_get_directory
- * to indicate if it really created a new directory or not, either
- * by a new out-argument, or by recording this information and
- * providing an accessor.
- */
- if (new_directory)
- notmuch_directory_set_mtime (directory, -1);
+ status = notmuch_database_get_directory (notmuch, path, &directory);
+ if (status) {
+ ret = status;
+ goto DONE;
+ }
+ db_mtime = directory ? notmuch_directory_get_mtime (directory) : 0;
/* If the database knows about this directory, then we sort based
* on strcmp to match the database sorting. Otherwise, we can do
* inode-based sorting for faster filesystem operation. */
num_fs_entries = scandir (path, &fs_entries, 0,
- new_directory ?
- dirent_sort_inode : dirent_sort_strcmp_name);
+ directory ?
+ dirent_sort_strcmp_name : dirent_sort_inode);
if (num_fs_entries == -1) {
fprintf (stderr, "Error opening directory %s: %s\n",
path, strerror (errno));
+ /* We consider this a fatal error because, if a user moved a
+ * message from another directory that we were able to scan
+ * into this directory, skipping this directory will cause
+ * that message to be lost. */
ret = NOTMUCH_STATUS_FILE_ERROR;
goto DONE;
}
@@ -351,8 +338,10 @@ add_files_recursive (notmuch_database_t *notmuch,
next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
status = add_files_recursive (notmuch, next, state);
- if (status && ret == NOTMUCH_STATUS_SUCCESS)
+ if (status) {
ret = status;
+ goto DONE;
+ }
talloc_free (next);
next = NULL;
}
@@ -366,13 +355,12 @@ add_files_recursive (notmuch_database_t *notmuch,
* being discovered until the clock catches up and the directory
* is modified again).
*/
- if (fs_mtime == db_mtime)
+ if (directory && fs_mtime == db_mtime)
goto DONE;
- /* new_directory means a directory that the database has never
- * seen before. In that case, we can simply leave db_files and
- * db_subdirs NULL. */
- if (!new_directory) {
+ /* If the database has never seen this directory before, we can
+ * simply leave db_files and db_subdirs NULL. */
+ if (directory) {
db_files = notmuch_directory_get_child_files (directory);
db_subdirs = notmuch_directory_get_child_directories (directory);
}
@@ -773,32 +761,40 @@ remove_filename (notmuch_database_t *notmuch,
return status;
status = notmuch_database_find_message_by_filename (notmuch, path, &message);
if (status || message == NULL)
- return status;
+ goto DONE;
+
status = notmuch_database_remove_message (notmuch, path);
if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
add_files_state->renamed_messages++;
if (add_files_state->synchronize_flags == TRUE)
notmuch_message_maildir_flags_to_tags (message);
- } else
+ status = NOTMUCH_STATUS_SUCCESS;
+ } else if (status == NOTMUCH_STATUS_SUCCESS) {
add_files_state->removed_messages++;
+ }
notmuch_message_destroy (message);
+
+ DONE:
notmuch_database_end_atomic (notmuch);
return status;
}
/* Recursively remove all filenames from the database referring to
* 'path' (or to any of its children). */
-static void
+static notmuch_status_t
_remove_directory (void *ctx,
notmuch_database_t *notmuch,
const char *path,
add_files_state_t *add_files_state)
{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
notmuch_directory_t *directory;
notmuch_filenames_t *files, *subdirs;
char *absolute;
- directory = notmuch_database_get_directory (notmuch, path);
+ status = notmuch_database_get_directory (notmuch, path, &directory);
+ if (status || !directory)
+ return status;
for (files = notmuch_directory_get_child_files (directory);
notmuch_filenames_valid (files);
@@ -806,8 +802,10 @@ _remove_directory (void *ctx,
{
absolute = talloc_asprintf (ctx, "%s/%s", path,
notmuch_filenames_get (files));
- remove_filename (notmuch, absolute, add_files_state);
+ status = remove_filename (notmuch, absolute, add_files_state);
talloc_free (absolute);
+ if (status)
+ goto DONE;
}
for (subdirs = notmuch_directory_get_child_directories (directory);
@@ -816,11 +814,15 @@ _remove_directory (void *ctx,
{
absolute = talloc_asprintf (ctx, "%s/%s", path,
notmuch_filenames_get (subdirs));
- _remove_directory (ctx, notmuch, absolute, add_files_state);
+ status = _remove_directory (ctx, notmuch, absolute, add_files_state);
talloc_free (absolute);
+ if (status)
+ goto DONE;
}
+ DONE:
notmuch_directory_destroy (directory);
+ return status;
}
int
@@ -882,12 +884,12 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
return 1;
printf ("Found %d total files (that's not much mail).\n", count);
- notmuch = notmuch_database_create (db_path);
+ if (notmuch_database_create (db_path, &notmuch))
+ return 1;
add_files_state.total_files = count;
} else {
- notmuch = notmuch_database_open (db_path,
- NOTMUCH_DATABASE_MODE_READ_WRITE);
- if (notmuch == NULL)
+ if (notmuch_database_open (db_path, NOTMUCH_DATABASE_MODE_READ_WRITE,
+ &notmuch))
return 1;
if (notmuch_database_needs_upgrade (notmuch)) {
@@ -933,10 +935,14 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
}
ret = add_files (notmuch, db_path, &add_files_state);
+ if (ret)
+ goto DONE;
gettimeofday (&tv_start, NULL);
for (f = add_files_state.removed_files->head; f && !interrupted; f = f->next) {
- remove_filename (notmuch, f->filename, &add_files_state);
+ ret = remove_filename (notmuch, f->filename, &add_files_state);
+ if (ret)
+ goto DONE;
if (do_print_progress) {
do_print_progress = 0;
generic_print_progress ("Cleaned up", "messages",
@@ -947,7 +953,9 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
gettimeofday (&tv_start, NULL);
for (f = add_files_state.removed_directories->head, i = 0; f && !interrupted; f = f->next, i++) {
- _remove_directory (ctx, notmuch, f->filename, &add_files_state);
+ ret = _remove_directory (ctx, notmuch, f->filename, &add_files_state);
+ if (ret)
+ goto DONE;
if (do_print_progress) {
do_print_progress = 0;
generic_print_progress ("Cleaned up", "directories",
@@ -957,14 +965,16 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
}
for (f = add_files_state.directory_mtimes->head; f && !interrupted; f = f->next) {
+ notmuch_status_t status;
notmuch_directory_t *directory;
- directory = notmuch_database_get_directory (notmuch, f->filename);
- if (directory) {
+ status = notmuch_database_get_directory (notmuch, f->filename, &directory);
+ if (status == NOTMUCH_STATUS_SUCCESS && directory) {
notmuch_directory_set_mtime (directory, f->mtime);
notmuch_directory_destroy (directory);
}
}
+ DONE:
talloc_free (add_files_state.removed_files);
talloc_free (add_files_state.removed_directories);
talloc_free (add_files_state.directory_mtimes);
@@ -1012,12 +1022,11 @@ notmuch_new_command (void *ctx, int argc, char *argv[])
printf ("\n");
- if (ret) {
- printf ("\nNote: At least one error was encountered: %s\n",
- notmuch_status_to_string (ret));
- }
+ if (ret)
+ fprintf (stderr, "Note: A fatal error was encountered: %s\n",
+ notmuch_status_to_string (ret));
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
if (run_hooks && !ret && !interrupted)
ret = notmuch_run_hook (db_path, "post-new");
diff --git a/notmuch-reply.c b/notmuch-reply.c
index 6b244e6d..7184a5df 100644
--- a/notmuch-reply.c
+++ b/notmuch-reply.c
@@ -21,32 +21,9 @@
*/
#include "notmuch-client.h"
-#include "gmime-filter-reply.h"
#include "gmime-filter-headers.h"
static void
-reply_headers_message_part (GMimeMessage *message);
-
-static void
-reply_part_content (GMimeObject *part);
-
-static const notmuch_show_format_t format_reply = {
- "", NULL,
- "", NULL,
- "", NULL, reply_headers_message_part, ">\n",
- "",
- NULL,
- NULL,
- NULL,
- reply_part_content,
- NULL,
- "",
- "",
- "", "",
- ""
-};
-
-static void
show_reply_headers (GMimeMessage *message)
{
GMimeStream *stream_stdout = NULL, *stream_filter = NULL;
@@ -66,85 +43,59 @@ show_reply_headers (GMimeMessage *message)
}
static void
-reply_headers_message_part (GMimeMessage *message)
+format_part_reply (mime_node_t *node)
{
- InternetAddressList *recipients;
- const char *recipients_string;
-
- printf ("> From: %s\n", g_mime_message_get_sender (message));
- recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
- recipients_string = internet_address_list_to_string (recipients, 0);
- if (recipients_string)
- printf ("> To: %s\n",
- recipients_string);
- recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
- recipients_string = internet_address_list_to_string (recipients, 0);
- if (recipients_string)
- printf ("> Cc: %s\n",
- recipients_string);
- printf ("> Subject: %s\n", g_mime_message_get_subject (message));
- printf ("> Date: %s\n", g_mime_message_get_date_as_string (message));
-}
-
+ int i;
-static void
-reply_part_content (GMimeObject *part)
-{
- GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
- GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part);
+ if (node->envelope_file) {
+ printf ("On %s, %s wrote:\n",
+ notmuch_message_get_header (node->envelope_file, "date"),
+ notmuch_message_get_header (node->envelope_file, "from"));
+ } else if (GMIME_IS_MESSAGE (node->part)) {
+ GMimeMessage *message = GMIME_MESSAGE (node->part);
+ InternetAddressList *recipients;
+ const char *recipients_string;
- if (g_mime_content_type_is_type (content_type, "multipart", "*") ||
- g_mime_content_type_is_type (content_type, "message", "rfc822"))
- {
- /* Output nothing, since multipart subparts will be handled individually. */
- }
- else if (g_mime_content_type_is_type (content_type, "application", "pgp-encrypted") ||
- g_mime_content_type_is_type (content_type, "application", "pgp-signature"))
- {
- /* Ignore PGP/MIME cruft parts */
- }
- else if (g_mime_content_type_is_type (content_type, "text", "*") &&
- !g_mime_content_type_is_type (content_type, "text", "html"))
- {
- GMimeStream *stream_stdout = NULL, *stream_filter = NULL;
- GMimeDataWrapper *wrapper;
- const char *charset;
+ printf ("> From: %s\n", g_mime_message_get_sender (message));
+ recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
+ recipients_string = internet_address_list_to_string (recipients, 0);
+ if (recipients_string)
+ printf ("> To: %s\n",
+ recipients_string);
+ recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
+ recipients_string = internet_address_list_to_string (recipients, 0);
+ if (recipients_string)
+ printf ("> Cc: %s\n",
+ recipients_string);
+ printf ("> Subject: %s\n", g_mime_message_get_subject (message));
+ printf ("> Date: %s\n", g_mime_message_get_date_as_string (message));
+ printf (">\n");
+ } else if (GMIME_IS_PART (node->part)) {
+ GMimeContentType *content_type = g_mime_object_get_content_type (node->part);
+ GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (node->part);
- charset = g_mime_object_get_content_type_parameter (part, "charset");
- stream_stdout = g_mime_stream_file_new (stdout);
- if (stream_stdout) {
+ if (g_mime_content_type_is_type (content_type, "application", "pgp-encrypted") ||
+ g_mime_content_type_is_type (content_type, "application", "pgp-signature")) {
+ /* Ignore PGP/MIME cruft parts */
+ } else if (g_mime_content_type_is_type (content_type, "text", "*") &&
+ !g_mime_content_type_is_type (content_type, "text", "html")) {
+ GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
- stream_filter = g_mime_stream_filter_new(stream_stdout);
- if (charset) {
- g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
- g_mime_filter_charset_new(charset, "UTF-8"));
- }
- }
- g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
- g_mime_filter_reply_new(TRUE));
- wrapper = g_mime_part_get_content_object (GMIME_PART (part));
- if (wrapper && stream_filter)
- g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
- if (stream_filter)
- g_object_unref(stream_filter);
- if (stream_stdout)
+ show_text_part_content (node->part, stream_stdout, NOTMUCH_SHOW_TEXT_PART_REPLY);
g_object_unref(stream_stdout);
- }
- else
- {
- if (disposition &&
- strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
- {
- const char *filename = g_mime_part_get_filename (GMIME_PART (part));
+ } else if (disposition &&
+ strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) {
+ const char *filename = g_mime_part_get_filename (GMIME_PART (node->part));
printf ("Attachment: %s (%s)\n", filename,
g_mime_content_type_to_string (content_type));
- }
- else
- {
+ } else {
printf ("Non-text part: %s\n",
g_mime_content_type_to_string (content_type));
}
}
+
+ for (i = 0; i < node->nchildren; i++)
+ format_part_reply (mime_node_child (node, i));
}
/* Is the given address configured as one of the user's "personal" or
@@ -505,6 +456,61 @@ guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message
return NULL;
}
+static GMimeMessage *
+create_reply_message(void *ctx,
+ notmuch_config_t *config,
+ notmuch_message_t *message,
+ notmuch_bool_t reply_all)
+{
+ const char *subject, *from_addr = NULL;
+ const char *in_reply_to, *orig_references, *references;
+
+ /* The 1 means we want headers in a "pretty" order. */
+ GMimeMessage *reply = g_mime_message_new (1);
+ if (reply == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return NULL;
+ }
+
+ subject = notmuch_message_get_header (message, "subject");
+ if (subject) {
+ if (strncasecmp (subject, "Re:", 3))
+ subject = talloc_asprintf (ctx, "Re: %s", subject);
+ g_mime_message_set_subject (reply, subject);
+ }
+
+ from_addr = add_recipients_from_message (reply, config,
+ message, reply_all);
+
+ if (from_addr == NULL)
+ from_addr = guess_from_received_header (config, message);
+
+ if (from_addr == NULL)
+ from_addr = notmuch_config_get_user_primary_email (config);
+
+ from_addr = talloc_asprintf (ctx, "%s <%s>",
+ notmuch_config_get_user_name (config),
+ from_addr);
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "From", from_addr);
+
+ in_reply_to = talloc_asprintf (ctx, "<%s>",
+ notmuch_message_get_message_id (message));
+
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "In-Reply-To", in_reply_to);
+
+ orig_references = notmuch_message_get_header (message, "references");
+ references = talloc_asprintf (ctx, "%s%s%s",
+ orig_references ? orig_references : "",
+ orig_references ? " " : "",
+ in_reply_to);
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "References", references);
+
+ return reply;
+}
+
static int
notmuch_reply_format_default(void *ctx,
notmuch_config_t *config,
@@ -515,9 +521,7 @@ notmuch_reply_format_default(void *ctx,
GMimeMessage *reply;
notmuch_messages_t *messages;
notmuch_message_t *message;
- const char *subject, *from_addr = NULL;
- const char *in_reply_to, *orig_references, *references;
- const notmuch_show_format_t *format = &format_reply;
+ mime_node_t *root;
for (messages = notmuch_query_search_messages (query);
notmuch_messages_valid (messages);
@@ -525,62 +529,74 @@ notmuch_reply_format_default(void *ctx,
{
message = notmuch_messages_get (messages);
- /* The 1 means we want headers in a "pretty" order. */
- reply = g_mime_message_new (1);
- if (reply == NULL) {
- fprintf (stderr, "Out of memory\n");
+ reply = create_reply_message (ctx, config, message, reply_all);
+
+ /* If reply creation failed, we're out of memory, so don't
+ * bother trying any more messages.
+ */
+ if (!reply) {
+ notmuch_message_destroy (message);
return 1;
}
- subject = notmuch_message_get_header (message, "subject");
- if (subject) {
- if (strncasecmp (subject, "Re:", 3))
- subject = talloc_asprintf (ctx, "Re: %s", subject);
- g_mime_message_set_subject (reply, subject);
- }
+ show_reply_headers (reply);
- from_addr = add_recipients_from_message (reply, config, message,
- reply_all);
+ g_object_unref (G_OBJECT (reply));
+ reply = NULL;
- if (from_addr == NULL)
- from_addr = guess_from_received_header (config, message);
+ if (mime_node_open (ctx, message, params->cryptoctx, params->decrypt,
+ &root) == NOTMUCH_STATUS_SUCCESS) {
+ format_part_reply (root);
+ talloc_free (root);
+ }
- if (from_addr == NULL)
- from_addr = notmuch_config_get_user_primary_email (config);
+ notmuch_message_destroy (message);
+ }
+ return 0;
+}
- from_addr = talloc_asprintf (ctx, "%s <%s>",
- notmuch_config_get_user_name (config),
- from_addr);
- g_mime_object_set_header (GMIME_OBJECT (reply),
- "From", from_addr);
+static int
+notmuch_reply_format_json(void *ctx,
+ notmuch_config_t *config,
+ notmuch_query_t *query,
+ notmuch_show_params_t *params,
+ notmuch_bool_t reply_all)
+{
+ GMimeMessage *reply;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ mime_node_t *node;
- in_reply_to = talloc_asprintf (ctx, "<%s>",
- notmuch_message_get_message_id (message));
+ if (notmuch_query_count_messages (query) != 1) {
+ fprintf (stderr, "Error: search term did not match precisely one message.\n");
+ return 1;
+ }
- g_mime_object_set_header (GMIME_OBJECT (reply),
- "In-Reply-To", in_reply_to);
+ messages = notmuch_query_search_messages (query);
+ message = notmuch_messages_get (messages);
+ if (mime_node_open (ctx, message, params->cryptoctx, params->decrypt,
+ &node) != NOTMUCH_STATUS_SUCCESS)
+ return 1;
- orig_references = notmuch_message_get_header (message, "references");
- references = talloc_asprintf (ctx, "%s%s%s",
- orig_references ? orig_references : "",
- orig_references ? " " : "",
- in_reply_to);
- g_mime_object_set_header (GMIME_OBJECT (reply),
- "References", references);
+ reply = create_reply_message (ctx, config, message, reply_all);
+ if (!reply)
+ return 1;
- show_reply_headers (reply);
+ /* The headers of the reply message we've created */
+ printf ("{\"reply-headers\": ");
+ format_headers_json (ctx, reply, TRUE);
+ g_object_unref (G_OBJECT (reply));
+ reply = NULL;
- g_object_unref (G_OBJECT (reply));
- reply = NULL;
+ /* Start the original */
+ printf (", \"original\": ");
- printf ("On %s, %s wrote:\n",
- notmuch_message_get_header (message, "date"),
- notmuch_message_get_header (message, "from"));
+ format_part_json (ctx, node, TRUE);
- show_message_body (message, format, params);
+ /* End */
+ printf ("}\n");
+ notmuch_message_destroy (message);
- notmuch_message_destroy (message);
- }
return 0;
}
@@ -646,6 +662,7 @@ notmuch_reply_format_headers_only(void *ctx,
enum {
FORMAT_DEFAULT,
+ FORMAT_JSON,
FORMAT_HEADERS_ONLY,
};
@@ -665,6 +682,7 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
notmuch_opt_desc_t options[] = {
{ NOTMUCH_OPT_KEYWORD, &format, "format", 'f',
(notmuch_keyword_t []){ { "default", FORMAT_DEFAULT },
+ { "json", FORMAT_JSON },
{ "headers-only", FORMAT_HEADERS_ONLY },
{ 0, 0 } } },
{ NOTMUCH_OPT_KEYWORD, &reply_all, "reply-to", 'r',
@@ -683,6 +701,8 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
if (format == FORMAT_HEADERS_ONLY)
reply_format_func = notmuch_reply_format_headers_only;
+ else if (format == FORMAT_JSON)
+ reply_format_func = notmuch_reply_format_json;
else
reply_format_func = notmuch_reply_format_default;
@@ -720,9 +740,8 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
return 1;
}
- notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY);
- if (notmuch == NULL)
+ if (notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
return 1;
query = notmuch_query_create (notmuch, query_string);
@@ -735,7 +754,7 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
return 1;
notmuch_query_destroy (query);
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
if (params.cryptoctx)
g_object_unref(params.cryptoctx);
diff --git a/notmuch-restore.c b/notmuch-restore.c
index 87d9772b..4f4096ed 100644
--- a/notmuch-restore.c
+++ b/notmuch-restore.c
@@ -20,6 +20,81 @@
#include "notmuch-client.h"
+static int
+tag_message (notmuch_database_t *notmuch, const char *message_id,
+ char *file_tags, notmuch_bool_t remove_all,
+ notmuch_bool_t synchronize_flags)
+{
+ notmuch_status_t status;
+ notmuch_tags_t *db_tags;
+ char *db_tags_str;
+ notmuch_message_t *message = NULL;
+ const char *tag;
+ char *next;
+ int ret = 0;
+
+ status = notmuch_database_find_message (notmuch, message_id, &message);
+ if (status || message == NULL) {
+ fprintf (stderr, "Warning: Cannot apply tags to %smessage: %s\n",
+ message ? "" : "missing ", message_id);
+ if (status)
+ fprintf (stderr, "%s\n", notmuch_status_to_string(status));
+ return 1;
+ }
+
+ /* In order to detect missing messages, this check/optimization is
+ * intentionally done *after* first finding the message. */
+ if (!remove_all && (file_tags == NULL || *file_tags == '\0'))
+ goto DONE;
+
+ db_tags_str = NULL;
+ for (db_tags = notmuch_message_get_tags (message);
+ notmuch_tags_valid (db_tags);
+ notmuch_tags_move_to_next (db_tags)) {
+ tag = notmuch_tags_get (db_tags);
+
+ if (db_tags_str)
+ db_tags_str = talloc_asprintf_append (db_tags_str, " %s", tag);
+ else
+ db_tags_str = talloc_strdup (message, tag);
+ }
+
+ if (((file_tags == NULL || *file_tags == '\0') &&
+ (db_tags_str == NULL || *db_tags_str == '\0')) ||
+ (file_tags && db_tags_str && strcmp (file_tags, db_tags_str) == 0))
+ goto DONE;
+
+ notmuch_message_freeze (message);
+
+ if (remove_all)
+ notmuch_message_remove_all_tags (message);
+
+ next = file_tags;
+ while (next) {
+ tag = strsep (&next, " ");
+ if (*tag == '\0')
+ continue;
+ status = notmuch_message_add_tag (message, tag);
+ if (status) {
+ fprintf (stderr, "Error applying tag %s to message %s:\n",
+ tag, message_id);
+ fprintf (stderr, "%s\n", notmuch_status_to_string (status));
+ ret = 1;
+ }
+ }
+
+ notmuch_message_thaw (message);
+
+ if (synchronize_flags)
+ notmuch_message_tags_to_maildir_flags (message);
+
+DONE:
+ if (message)
+ notmuch_message_destroy (message);
+
+ return ret;
+}
+
int
notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
{
@@ -40,9 +115,8 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
if (config == NULL)
return 1;
- notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_WRITE);
- if (notmuch == NULL)
+ if (notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
return 1;
synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
@@ -88,11 +162,7 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
while ((line_len = getline (&line, &line_size, input)) != -1) {
regmatch_t match[3];
- char *message_id, *file_tags, *tag, *next;
- notmuch_message_t *message = NULL;
- notmuch_status_t status;
- notmuch_tags_t *db_tags;
- char *db_tags_str;
+ char *message_id, *file_tags;
chomp_newline (line);
@@ -109,72 +179,9 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
file_tags = xstrndup (line + match[2].rm_so,
match[2].rm_eo - match[2].rm_so);
- status = notmuch_database_find_message (notmuch, message_id, &message);
- if (status || message == NULL) {
- fprintf (stderr, "Warning: Cannot apply tags to %smessage: %s\n",
- message ? "" : "missing ", message_id);
- if (status)
- fprintf (stderr, "%s\n",
- notmuch_status_to_string(status));
- goto NEXT_LINE;
- }
-
- /* In order to detect missing messages, this check/optimization is
- * intentionally done *after* first finding the message. */
- if (accumulate && (file_tags == NULL || *file_tags == '\0'))
- {
- goto NEXT_LINE;
- }
-
- db_tags_str = NULL;
- for (db_tags = notmuch_message_get_tags (message);
- notmuch_tags_valid (db_tags);
- notmuch_tags_move_to_next (db_tags))
- {
- const char *tag = notmuch_tags_get (db_tags);
-
- if (db_tags_str)
- db_tags_str = talloc_asprintf_append (db_tags_str, " %s", tag);
- else
- db_tags_str = talloc_strdup (message, tag);
- }
-
- if (((file_tags == NULL || *file_tags == '\0') &&
- (db_tags_str == NULL || *db_tags_str == '\0')) ||
- (file_tags && db_tags_str && strcmp (file_tags, db_tags_str) == 0))
- {
- goto NEXT_LINE;
- }
-
- notmuch_message_freeze (message);
-
- if (!accumulate)
- notmuch_message_remove_all_tags (message);
-
- next = file_tags;
- while (next) {
- tag = strsep (&next, " ");
- if (*tag == '\0')
- continue;
- status = notmuch_message_add_tag (message, tag);
- if (status) {
- fprintf (stderr,
- "Error applying tag %s to message %s:\n",
- tag, message_id);
- fprintf (stderr, "%s\n",
- notmuch_status_to_string (status));
- }
- }
-
- notmuch_message_thaw (message);
-
- if (synchronize_flags)
- notmuch_message_tags_to_maildir_flags (message);
+ tag_message (notmuch, message_id, file_tags, !accumulate,
+ synchronize_flags);
- NEXT_LINE:
- if (message)
- notmuch_message_destroy (message);
- message = NULL;
free (message_id);
free (file_tags);
}
@@ -184,7 +191,7 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
if (line)
free (line);
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
if (input != stdin)
fclose (input);
diff --git a/notmuch-search.c b/notmuch-search.c
index 92ce38a1..3be296d8 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -371,6 +371,9 @@ do_search_tags (notmuch_database_t *notmuch,
const char *tag;
int first_tag = 1;
+ /* should the following only special case if no excluded terms
+ * specified? */
+
/* Special-case query of "*" for better performance. */
if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
tags = notmuch_database_get_all_tags (notmuch);
@@ -413,6 +416,12 @@ do_search_tags (notmuch_database_t *notmuch,
return 0;
}
+enum {
+ EXCLUDE_TRUE,
+ EXCLUDE_FALSE,
+ EXCLUDE_FLAG,
+};
+
int
notmuch_search_command (void *ctx, int argc, char *argv[])
{
@@ -426,8 +435,7 @@ 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;
+ int exclude = EXCLUDE_TRUE;
unsigned int i;
enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT }
@@ -449,6 +457,11 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
{ "files", OUTPUT_FILES },
{ "tags", OUTPUT_TAGS },
{ 0, 0 } } },
+ { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
+ (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
+ { "false", EXCLUDE_FALSE },
+ { "flag", EXCLUDE_FLAG },
+ { 0, 0 } } },
{ NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
{ NOTMUCH_OPT_INT, &limit, "limit", 'L', 0 },
{ 0, 0, 0, 0, 0 }
@@ -473,9 +486,8 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
if (config == NULL)
return 1;
- notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY);
- if (notmuch == NULL)
+ if (notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
return 1;
query_str = query_string_from_args (notmuch, argc-opt_index, argv+opt_index);
@@ -496,10 +508,25 @@ 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]);
+ if (exclude == EXCLUDE_FLAG && output != OUTPUT_SUMMARY) {
+ /* If we are not doing summary output there is nowhere to
+ * print the excluded flag so fall back on including the
+ * excluded messages. */
+ fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
+ exclude = EXCLUDE_FALSE;
+ }
+
+ if (exclude == EXCLUDE_TRUE || exclude == EXCLUDE_FLAG) {
+ const char **search_exclude_tags;
+ size_t search_exclude_tags_length;
+
+ 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]);
+ if (exclude == EXCLUDE_FLAG)
+ notmuch_query_set_omit_excluded (query, FALSE);
+ }
switch (output) {
default:
@@ -517,7 +544,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
}
notmuch_query_destroy (query);
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
return ret;
}
diff --git a/notmuch-setup.c b/notmuch-setup.c
index 307231d5..94d0aa7b 100644
--- a/notmuch-setup.c
+++ b/notmuch-setup.c
@@ -133,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 { \
@@ -209,7 +211,22 @@ notmuch_setup_command (unused (void *ctx),
}
- /* Temporarily remove exclude tag support for 0.12 */
+ 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 = parse_tag_list (ctx, response);
+
+ 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)
diff --git a/notmuch-show.c b/notmuch-show.c
index 93fb16f3..95427d4f 100644
--- a/notmuch-show.c
+++ b/notmuch-show.c
@@ -19,108 +19,42 @@
*/
#include "notmuch-client.h"
+#include "gmime-filter-reply.h"
-static void
-format_headers_message_part_text (GMimeMessage *message);
-
-static void
+static notmuch_status_t
format_part_text (const void *ctx, mime_node_t *node,
int indent, const notmuch_show_params_t *params);
static const notmuch_show_format_t format_text = {
- .message_set_start = "",
.part = format_part_text,
- .message_set_sep = "",
- .message_set_end = ""
};
-static void
-format_message_json (const void *ctx,
- notmuch_message_t *message,
- unused (int indent));
-static void
-format_headers_json (const void *ctx,
- notmuch_message_t *message);
-
-static void
-format_headers_message_part_json (GMimeMessage *message);
-
-static void
-format_part_start_json (unused (GMimeObject *part),
- int *part_count);
-
-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);
-
-static void
-format_part_end_json (GMimeObject *part);
+static notmuch_status_t
+format_part_json_entry (const void *ctx, mime_node_t *node,
+ int indent, const notmuch_show_params_t *params);
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
static const notmuch_show_format_t format_json = {
- "[", NULL,
- "{", format_message_json,
- "\"headers\": {", format_headers_json, format_headers_message_part_json, "}",
- ", \"body\": [",
- format_part_start_json,
- format_part_encstatus_json,
- format_part_sigstatus_json,
- format_part_content_json,
- format_part_end_json,
- ", ",
- "]",
- "}", ", ",
- "]"
+ .message_set_start = "[",
+ .part = format_part_json_entry,
+ .message_set_sep = ", ",
+ .message_set_end = "]"
};
-static void
-format_message_mbox (const void *ctx,
- notmuch_message_t *message,
- unused (int indent));
+static notmuch_status_t
+format_part_mbox (const void *ctx, mime_node_t *node,
+ int indent, const notmuch_show_params_t *params);
static const notmuch_show_format_t format_mbox = {
- "", NULL,
- "", format_message_mbox,
- "", NULL, NULL, "",
- "",
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- "",
- "",
- "", "",
- ""
+ .part = format_part_mbox,
};
-static void
-format_part_content_raw (GMimeObject *part);
+static notmuch_status_t
+format_part_raw (unused (const void *ctx), mime_node_t *node,
+ unused (int indent),
+ unused (const notmuch_show_params_t *params));
static const notmuch_show_format_t format_raw = {
- "", NULL,
- "", NULL,
- "", NULL, format_headers_message_part_text, "\n",
- "",
- NULL,
- NULL,
- NULL,
- format_part_content_raw,
- NULL,
- "",
- "",
- "", "",
- ""
+ .part = format_part_raw,
};
static const char *
@@ -170,7 +104,7 @@ _get_one_line_summary (const void *ctx, notmuch_message_t *message)
}
static void
-format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))
+format_message_json (const void *ctx, notmuch_message_t *message)
{
notmuch_tags_t *tags;
int first = 1;
@@ -181,9 +115,10 @@ format_message_json (const void *ctx, notmuch_message_t *message, unused (int in
date = notmuch_message_get_date (message);
relative_date = notmuch_time_relative_date (ctx, date);
- printf ("\"id\": %s, \"match\": %s, \"filename\": %s, \"timestamp\": %ld, \"date_relative\": \"%s\", \"tags\": [",
+ printf ("\"id\": %s, \"match\": %s, \"excluded\": %s, \"filename\": %s, \"timestamp\": %ld, \"date_relative\": \"%s\", \"tags\": [",
json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),
notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",
+ notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? "true" : "false",
json_quote_str (ctx_quote, notmuch_message_get_filename (message)),
date, relative_date);
@@ -257,149 +192,64 @@ _is_from_line (const char *line)
return 0;
}
-/* Print a message in "mboxrd" format as documented, for example,
- * here:
- *
- * http://qmail.org/qmail-manual-html/man5/mbox.html
- */
-static void
-format_message_mbox (const void *ctx,
- notmuch_message_t *message,
- unused (int indent))
-{
- const char *filename;
- FILE *file;
- const char *from;
-
- time_t date;
- struct tm date_gmtime;
- char date_asctime[26];
-
- char *line = NULL;
- size_t line_size;
- ssize_t line_len;
-
- filename = notmuch_message_get_filename (message);
- file = fopen (filename, "r");
- if (file == NULL) {
- fprintf (stderr, "Failed to open %s: %s\n",
- filename, strerror (errno));
- return;
- }
-
- from = notmuch_message_get_header (message, "from");
- from = _extract_email_address (ctx, from);
-
- date = notmuch_message_get_date (message);
- gmtime_r (&date, &date_gmtime);
- asctime_r (&date_gmtime, date_asctime);
-
- printf ("From %s %s", from, date_asctime);
-
- while ((line_len = getline (&line, &line_size, file)) != -1 ) {
- if (_is_from_line (line))
- putchar ('>');
- printf ("%s", line);
- }
-
- printf ("\n");
-
- fclose (file);
-}
-
-static void
-format_headers_message_part_text (GMimeMessage *message)
+void
+format_headers_json (const void *ctx, GMimeMessage *message, notmuch_bool_t reply)
{
+ void *local = talloc_new (ctx);
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));
+ printf ("{%s: %s",
+ json_quote_str (local, "Subject"),
+ json_quote_str (local, g_mime_message_get_subject (message)));
+ printf (", %s: %s",
+ json_quote_str (local, "From"),
+ json_quote_str (local, g_mime_message_get_sender (message)));
recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
recipients_string = internet_address_list_to_string (recipients, 0);
if (recipients_string)
- printf ("To: %s\n",
- recipients_string);
+ printf (", %s: %s",
+ json_quote_str (local, "To"),
+ json_quote_str (local, recipients_string));
recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
recipients_string = internet_address_list_to_string (recipients, 0);
if (recipients_string)
- printf ("Cc: %s\n",
- recipients_string);
- printf ("Date: %s\n", g_mime_message_get_date_as_string (message));
-}
-
-static void
-format_headers_json (const void *ctx, notmuch_message_t *message)
-{
- const char *headers[] = {
- "Subject", "From", "To", "Cc", "Bcc", "Date"
- };
- const char *name, *value;
- unsigned int i;
- int first_header = 1;
- void *ctx_quote = talloc_new (ctx);
-
- for (i = 0; i < ARRAY_SIZE (headers); i++) {
- name = headers[i];
- value = notmuch_message_get_header (message, name);
- if (value)
- {
- if (!first_header)
- fputs (", ", stdout);
- first_header = 0;
-
- printf ("%s: %s",
- json_quote_str (ctx_quote, name),
- json_quote_str (ctx_quote, value));
- }
- }
-
- talloc_free (ctx_quote);
-}
+ printf (", %s: %s",
+ json_quote_str (local, "Cc"),
+ json_quote_str (local, recipients_string));
-static void
-format_headers_message_part_json (GMimeMessage *message)
-{
- void *ctx = talloc_new (NULL);
- void *ctx_quote = talloc_new (ctx);
- InternetAddressList *recipients;
- const char *recipients_string;
+ if (reply) {
+ printf (", %s: %s",
+ json_quote_str (local, "In-reply-to"),
+ json_quote_str (local, g_mime_object_get_header (GMIME_OBJECT (message), "In-reply-to")));
- printf ("%s: %s",
- json_quote_str (ctx_quote, "From"),
- json_quote_str (ctx_quote, g_mime_message_get_sender (message)));
- recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
- recipients_string = internet_address_list_to_string (recipients, 0);
- if (recipients_string)
printf (", %s: %s",
- json_quote_str (ctx_quote, "To"),
- json_quote_str (ctx_quote, recipients_string));
- recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
- recipients_string = internet_address_list_to_string (recipients, 0);
- if (recipients_string)
+ json_quote_str (local, "References"),
+ json_quote_str (local, g_mime_object_get_header (GMIME_OBJECT (message), "References")));
+ } else {
printf (", %s: %s",
- json_quote_str (ctx_quote, "Cc"),
- json_quote_str (ctx_quote, recipients_string));
- printf (", %s: %s",
- json_quote_str (ctx_quote, "Subject"),
- json_quote_str (ctx_quote, g_mime_message_get_subject (message)));
- printf (", %s: %s",
- json_quote_str (ctx_quote, "Date"),
- json_quote_str (ctx_quote, g_mime_message_get_date_as_string (message)));
+ json_quote_str (local, "Date"),
+ json_quote_str (local, g_mime_message_get_date_as_string (message)));
+ }
- talloc_free (ctx_quote);
- talloc_free (ctx);
+ printf ("}");
+
+ talloc_free (local);
}
/* Write a MIME text part out to the given stream.
*
+ * If (flags & NOTMUCH_SHOW_TEXT_PART_REPLY), this prepends "> " to
+ * each output line.
+ *
* Both line-ending conversion (CRLF->LF) and charset conversion ( ->
* UTF-8) will be performed, so it is inappropriate to call this
* function with a non-text part. Doing so will trigger an internal
* error.
*/
-static void
-show_text_part_content (GMimeObject *part, GMimeStream *stream_out)
+void
+show_text_part_content (GMimeObject *part, GMimeStream *stream_out,
+ notmuch_show_text_part_flags flags)
{
GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
GMimeStream *stream_filter = NULL;
@@ -432,6 +282,16 @@ show_text_part_content (GMimeObject *part, GMimeStream *stream_out)
}
+ if (flags & NOTMUCH_SHOW_TEXT_PART_REPLY) {
+ GMimeFilter *reply_filter;
+ reply_filter = g_mime_filter_reply_new (TRUE);
+ if (reply_filter) {
+ g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
+ reply_filter);
+ g_object_unref (reply_filter);
+ }
+ }
+
wrapper = g_mime_part_get_content_object (GMIME_PART (part));
if (wrapper && stream_filter)
g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
@@ -471,29 +331,13 @@ signer_status_to_string (GMimeSignerStatus x)
}
#endif
-static void
-format_part_start_json (unused (GMimeObject *part), int *part_count)
-{
- printf ("{\"id\": %d", *part_count);
-}
-
-static void
-format_part_encstatus_json (int status)
-{
- printf (", \"encstatus\": [{\"status\": ");
- if (status) {
- printf ("\"good\"");
- } else {
- printf ("\"bad\"");
- }
- printf ("}]");
-}
-
#ifdef GMIME_ATLEAST_26
static void
-format_part_sigstatus_json (GMimeSignatureList *siglist)
+format_part_sigstatus_json (mime_node_t *node)
{
- printf (", \"sigstatus\": [");
+ GMimeSignatureList *siglist = node->sig_list;
+
+ printf ("[");
if (!siglist) {
printf ("]");
@@ -557,9 +401,11 @@ format_part_sigstatus_json (GMimeSignatureList *siglist)
}
#else
static void
-format_part_sigstatus_json (const GMimeSignatureValidity* validity)
+format_part_sigstatus_json (mime_node_t *node)
{
- printf (", \"sigstatus\": [");
+ const GMimeSignatureValidity* validity = node->sig_validity;
+
+ printf ("[");
if (!validity) {
printf ("]");
@@ -618,109 +464,7 @@ format_part_sigstatus_json (const GMimeSignatureValidity* validity)
}
#endif
-static void
-format_part_content_json (GMimeObject *part)
-{
- GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
- GMimeStream *stream_memory = g_mime_stream_mem_new ();
- const char *cid = g_mime_object_get_content_id (part);
- void *ctx = talloc_new (NULL);
- GByteArray *part_content;
-
- printf (", \"content-type\": %s",
- json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
-
- if (cid != NULL)
- printf(", \"content-id\": %s", json_quote_str (ctx, cid));
-
- if (GMIME_IS_PART (part))
- {
- const char *filename = g_mime_part_get_filename (GMIME_PART (part));
- if (filename)
- printf (", \"filename\": %s", json_quote_str (ctx, filename));
- }
-
- if (g_mime_content_type_is_type (content_type, "text", "*"))
- {
- /* 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));
- }
- }
- else if (g_mime_content_type_is_type (content_type, "multipart", "*"))
- {
- printf (", \"content\": [");
- }
- else if (g_mime_content_type_is_type (content_type, "message", "rfc822"))
- {
- printf (", \"content\": [{");
- }
-
- talloc_free (ctx);
- if (stream_memory)
- g_object_unref (stream_memory);
-}
-
-static void
-format_part_end_json (GMimeObject *part)
-{
- GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
-
- if (g_mime_content_type_is_type (content_type, "multipart", "*"))
- printf ("]");
- else if (g_mime_content_type_is_type (content_type, "message", "rfc822"))
- printf ("}]");
-
- printf ("}");
-}
-
-static void
-format_part_content_raw (GMimeObject *part)
-{
- if (! GMIME_IS_PART (part))
- return;
-
- GMimeStream *stream_stdout;
- GMimeStream *stream_filter = NULL;
- GMimeDataWrapper *wrapper;
-
- stream_stdout = g_mime_stream_file_new (stdout);
- g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
-
- stream_filter = g_mime_stream_filter_new (stream_stdout);
-
- wrapper = g_mime_part_get_content_object (GMIME_PART (part));
-
- if (wrapper && stream_filter)
- g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
-
- if (stream_filter)
- g_object_unref (stream_filter);
-
- if (stream_stdout)
- g_object_unref(stream_stdout);
-}
-
-static void
+static notmuch_status_t
format_part_text (const void *ctx, mime_node_t *node,
int indent, const notmuch_show_params_t *params)
{
@@ -737,11 +481,12 @@ format_part_text (const void *ctx, mime_node_t *node,
notmuch_message_t *message = node->envelope_file;
part_type = "message";
- printf ("\f%s{ id:%s depth:%d match:%d filename:%s\n",
+ printf ("\f%s{ id:%s depth:%d match:%d excluded:%d filename:%s\n",
part_type,
notmuch_message_get_message_id (message),
indent,
- notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
+ notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? 1 : 0,
+ notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? 1 : 0,
notmuch_message_get_filename (message));
} else {
GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (meta);
@@ -793,7 +538,7 @@ format_part_text (const void *ctx, mime_node_t *node,
{
GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
- show_text_part_content (node->part, stream_stdout);
+ show_text_part_content (node->part, stream_stdout, 0);
g_object_unref(stream_stdout);
} else {
printf ("Non-text part: %s\n",
@@ -808,52 +553,276 @@ format_part_text (const void *ctx, mime_node_t *node,
printf ("\fbody}\n");
printf ("\f%s}\n", part_type);
+
+ return NOTMUCH_STATUS_SUCCESS;
}
-static void
-show_message (void *ctx,
- const notmuch_show_format_t *format,
- notmuch_message_t *message,
- int indent,
- notmuch_show_params_t *params)
+void
+format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first)
{
- if (format->part) {
- void *local = talloc_new (ctx);
- mime_node_t *root, *part;
+ /* Any changes to the JSON format should be reflected in the file
+ * devel/schemata. */
+
+ if (node->envelope_file) {
+ printf ("{");
+ format_message_json (ctx, node->envelope_file);
- 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);
+ printf ("\"headers\": ");
+ format_headers_json (ctx, GMIME_MESSAGE (node->part), FALSE);
+
+ printf (", \"body\": [");
+ format_part_json (ctx, mime_node_child (node, 0), first);
+
+ printf ("]}");
return;
}
- if (params->part <= 0) {
- fputs (format->message_start, stdout);
- if (format->message)
- format->message(ctx, message, indent);
+ void *local = talloc_new (ctx);
+ /* The disposition and content-type metadata are associated with
+ * the envelope for message parts */
+ GMimeObject *meta = node->envelope_part ?
+ GMIME_OBJECT (node->envelope_part) : node->part;
+ GMimeContentType *content_type = g_mime_object_get_content_type (meta);
+ const char *cid = g_mime_object_get_content_id (meta);
+ const char *filename = GMIME_IS_PART (node->part) ?
+ g_mime_part_get_filename (GMIME_PART (node->part)) : NULL;
+ const char *terminator = "";
+ int i;
+
+ if (!first)
+ printf (", ");
- fputs (format->header_start, stdout);
- if (format->header)
- format->header(ctx, message);
- fputs (format->header_end, stdout);
+ printf ("{\"id\": %d", node->part_num);
- fputs (format->body_start, stdout);
+ if (node->decrypt_attempted)
+ printf (", \"encstatus\": [{\"status\": \"%s\"}]",
+ node->decrypt_success ? "good" : "bad");
+
+ if (node->verify_attempted) {
+ printf (", \"sigstatus\": ");
+ format_part_sigstatus_json (node);
}
- if (format->part_content)
- show_message_body (message, format, params);
+ printf (", \"content-type\": %s",
+ json_quote_str (local, g_mime_content_type_to_string (content_type)));
+
+ if (cid)
+ printf (", \"content-id\": %s", json_quote_str (local, cid));
+
+ if (filename)
+ printf (", \"filename\": %s", json_quote_str (local, filename));
+
+ if (GMIME_IS_PART (node->part)) {
+ /* 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 (meta, "charset");
+
+ if (content_charset != NULL)
+ printf (", \"content-charset\": %s", json_quote_str (local, content_charset));
+ } else if (g_mime_content_type_is_type (content_type, "text", "*")) {
+ GMimeStream *stream_memory = g_mime_stream_mem_new ();
+ GByteArray *part_content;
+ show_text_part_content (node->part, stream_memory, 0);
+ part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
- if (params->part <= 0) {
- fputs (format->body_end, stdout);
+ printf (", \"content\": %s", json_quote_chararray (local, (char *) part_content->data, part_content->len));
+ g_object_unref (stream_memory);
+ }
+ } else if (GMIME_IS_MULTIPART (node->part)) {
+ printf (", \"content\": [");
+ terminator = "]";
+ } else if (GMIME_IS_MESSAGE (node->part)) {
+ printf (", \"content\": [{");
+ printf ("\"headers\": ");
+ format_headers_json (local, GMIME_MESSAGE (node->part), FALSE);
- fputs (format->message_end, stdout);
+ printf (", \"body\": [");
+ terminator = "]}]";
}
+
+ talloc_free (local);
+
+ for (i = 0; i < node->nchildren; i++)
+ format_part_json (ctx, mime_node_child (node, i), i == 0);
+
+ printf ("%s}", terminator);
}
-static void
+static notmuch_status_t
+format_part_json_entry (const void *ctx, mime_node_t *node, unused (int indent),
+ unused (const notmuch_show_params_t *params))
+{
+ format_part_json (ctx, node, TRUE);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Print a message in "mboxrd" format as documented, for example,
+ * here:
+ *
+ * http://qmail.org/qmail-manual-html/man5/mbox.html
+ */
+static notmuch_status_t
+format_part_mbox (const void *ctx, mime_node_t *node, unused (int indent),
+ unused (const notmuch_show_params_t *params))
+{
+ notmuch_message_t *message = node->envelope_file;
+
+ const char *filename;
+ FILE *file;
+ const char *from;
+
+ time_t date;
+ struct tm date_gmtime;
+ char date_asctime[26];
+
+ char *line = NULL;
+ size_t line_size;
+ ssize_t line_len;
+
+ if (!message)
+ INTERNAL_ERROR ("format_part_mbox requires a root part");
+
+ filename = notmuch_message_get_filename (message);
+ file = fopen (filename, "r");
+ if (file == NULL) {
+ fprintf (stderr, "Failed to open %s: %s\n",
+ filename, strerror (errno));
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+
+ from = notmuch_message_get_header (message, "from");
+ from = _extract_email_address (ctx, from);
+
+ date = notmuch_message_get_date (message);
+ gmtime_r (&date, &date_gmtime);
+ asctime_r (&date_gmtime, date_asctime);
+
+ printf ("From %s %s", from, date_asctime);
+
+ while ((line_len = getline (&line, &line_size, file)) != -1 ) {
+ if (_is_from_line (line))
+ putchar ('>');
+ printf ("%s", line);
+ }
+
+ printf ("\n");
+
+ fclose (file);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+format_part_raw (unused (const void *ctx), mime_node_t *node,
+ unused (int indent),
+ unused (const notmuch_show_params_t *params))
+{
+ if (node->envelope_file) {
+ /* Special case the entire message to avoid MIME parsing. */
+ const char *filename;
+ FILE *file;
+ size_t size;
+ char buf[4096];
+
+ filename = notmuch_message_get_filename (node->envelope_file);
+ if (filename == NULL) {
+ fprintf (stderr, "Error: Cannot get message filename.\n");
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+
+ file = fopen (filename, "r");
+ if (file == NULL) {
+ fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+
+ while (!feof (file)) {
+ size = fread (buf, 1, sizeof (buf), file);
+ if (ferror (file)) {
+ fprintf (stderr, "Error: Read failed from %s\n", filename);
+ fclose (file);
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+
+ if (fwrite (buf, size, 1, stdout) != 1) {
+ fprintf (stderr, "Error: Write failed\n");
+ fclose (file);
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+ }
+
+ fclose (file);
+ return NOTMUCH_STATUS_SUCCESS;
+ }
+
+ GMimeStream *stream_stdout;
+ GMimeStream *stream_filter = NULL;
+
+ stream_stdout = g_mime_stream_file_new (stdout);
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+
+ stream_filter = g_mime_stream_filter_new (stream_stdout);
+
+ if (GMIME_IS_PART (node->part)) {
+ /* For leaf parts, we emit only the transfer-decoded
+ * body. */
+ GMimeDataWrapper *wrapper;
+ wrapper = g_mime_part_get_content_object (GMIME_PART (node->part));
+
+ if (wrapper && stream_filter)
+ g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+ } else {
+ /* Write out the whole part. For message parts (the root
+ * part and embedded message parts), this will be the
+ * message including its headers (but not the
+ * encapsulating part's headers). For multipart parts,
+ * this will include the headers. */
+ if (stream_filter)
+ g_mime_object_write_to_stream (node->part, stream_filter);
+ }
+
+ if (stream_filter)
+ g_object_unref (stream_filter);
+
+ if (stream_stdout)
+ g_object_unref(stream_stdout);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+show_message (void *ctx,
+ const notmuch_show_format_t *format,
+ notmuch_message_t *message,
+ int indent,
+ notmuch_show_params_t *params)
+{
+ void *local = talloc_new (ctx);
+ mime_node_t *root, *part;
+ notmuch_status_t status;
+
+ status = mime_node_open (local, message, params->cryptoctx,
+ params->decrypt, &root);
+ if (status)
+ goto DONE;
+ part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
+ if (part)
+ status = format->part (local, part, indent, params);
+ DONE:
+ talloc_free (local);
+ return status;
+}
+
+static notmuch_status_t
show_messages (void *ctx,
const notmuch_show_format_t *format,
notmuch_messages_t *messages,
@@ -862,46 +831,60 @@ show_messages (void *ctx,
{
notmuch_message_t *message;
notmuch_bool_t match;
+ notmuch_bool_t excluded;
int first_set = 1;
int next_indent;
+ notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
- fputs (format->message_set_start, stdout);
+ if (format->message_set_start)
+ fputs (format->message_set_start, stdout);
for (;
notmuch_messages_valid (messages);
notmuch_messages_move_to_next (messages))
{
- if (!first_set)
+ if (!first_set && format->message_set_sep)
fputs (format->message_set_sep, stdout);
first_set = 0;
- fputs (format->message_set_start, stdout);
+ if (format->message_set_start)
+ fputs (format->message_set_start, stdout);
message = notmuch_messages_get (messages);
match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
+ excluded = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
next_indent = indent;
- if (match || params->entire_thread) {
- show_message (ctx, format, message, indent, params);
+ if ((match && (!excluded || !params->omit_excluded)) || params->entire_thread) {
+ status = show_message (ctx, format, message, indent, params);
+ if (status && !res)
+ res = status;
next_indent = indent + 1;
- fputs (format->message_set_sep, stdout);
+ if (!status && format->message_set_sep)
+ fputs (format->message_set_sep, stdout);
}
- show_messages (ctx,
- format,
- notmuch_message_get_replies (message),
- next_indent,
- params);
+ status = show_messages (ctx,
+ format,
+ notmuch_message_get_replies (message),
+ next_indent,
+ params);
+ if (status && !res)
+ res = status;
notmuch_message_destroy (message);
- fputs (format->message_set_end, stdout);
+ if (format->message_set_end)
+ fputs (format->message_set_end, stdout);
}
- fputs (format->message_set_end, stdout);
+ if (format->message_set_end)
+ fputs (format->message_set_end, stdout);
+
+ return res;
}
/* Formatted output of single message */
@@ -929,50 +912,7 @@ do_show_single (void *ctx,
notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
- /* Special case for --format=raw of full single message, just cat out file */
- if (params->raw && 0 == params->part) {
-
- const char *filename;
- FILE *file;
- size_t size;
- char buf[4096];
-
- filename = notmuch_message_get_filename (message);
- if (filename == NULL) {
- fprintf (stderr, "Error: Cannot message filename.\n");
- return 1;
- }
-
- file = fopen (filename, "r");
- if (file == NULL) {
- fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
- return 1;
- }
-
- while (!feof (file)) {
- size = fread (buf, 1, sizeof (buf), file);
- 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);
-
- } else {
-
- show_message (ctx, format, message, 0, params);
-
- }
-
- return 0;
+ return show_message (ctx, format, message, 0, params) != NOTMUCH_STATUS_SUCCESS;
}
/* Formatted output of threads */
@@ -986,8 +926,10 @@ do_show (void *ctx,
notmuch_thread_t *thread;
notmuch_messages_t *messages;
int first_toplevel = 1;
+ notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
- fputs (format->message_set_start, stdout);
+ if (format->message_set_start)
+ fputs (format->message_set_start, stdout);
for (threads = notmuch_query_search_threads (query);
notmuch_threads_valid (threads);
@@ -1001,19 +943,22 @@ do_show (void *ctx,
INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
notmuch_thread_get_thread_id (thread));
- if (!first_toplevel)
+ if (!first_toplevel && format->message_set_sep)
fputs (format->message_set_sep, stdout);
first_toplevel = 0;
- show_messages (ctx, format, messages, 0, params);
+ status = show_messages (ctx, format, messages, 0, params);
+ if (status && !res)
+ res = status;
notmuch_thread_destroy (thread);
}
- fputs (format->message_set_end, stdout);
+ if (format->message_set_end)
+ fputs (format->message_set_end, stdout);
- return 0;
+ return res != NOTMUCH_STATUS_SUCCESS;
}
enum {
@@ -1024,6 +969,12 @@ enum {
NOTMUCH_FORMAT_RAW
};
+/* The following is to allow future options to be added more easily */
+enum {
+ EXCLUDE_TRUE,
+ EXCLUDE_FALSE,
+};
+
int
notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
{
@@ -1033,9 +984,10 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
char *query_string;
int opt_index, ret;
const notmuch_show_format_t *format = &format_text;
- notmuch_show_params_t params = { .part = -1 };
+ notmuch_show_params_t params = { .part = -1, .omit_excluded = TRUE };
int format_sel = NOTMUCH_FORMAT_NOT_SPECIFIED;
notmuch_bool_t verify = FALSE;
+ int exclude = EXCLUDE_TRUE;
notmuch_opt_desc_t options[] = {
{ NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
@@ -1044,6 +996,10 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
{ "mbox", NOTMUCH_FORMAT_MBOX },
{ "raw", NOTMUCH_FORMAT_RAW },
{ 0, 0 } } },
+ { NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
+ (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
+ { "false", EXCLUDE_FALSE },
+ { 0, 0 } } },
{ NOTMUCH_OPT_INT, &params.part, "part", 'p', 0 },
{ NOTMUCH_OPT_BOOLEAN, &params.entire_thread, "entire-thread", 't', 0 },
{ NOTMUCH_OPT_BOOLEAN, &params.decrypt, "decrypt", 'd', 0 },
@@ -1078,6 +1034,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
return 1;
}
+
format = &format_mbox;
break;
case NOTMUCH_FORMAT_RAW:
@@ -1124,9 +1081,8 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
return 1;
}
- notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY);
- if (notmuch == NULL)
+ if (notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch))
return 1;
query = notmuch_query_create (notmuch, query_string);
@@ -1135,13 +1091,32 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
return 1;
}
+ /* If a single message is requested we do not use search_excludes. */
if (params.part >= 0)
ret = do_show_single (ctx, query, format, &params);
- else
+ else {
+ /* We always apply set the exclude flag. The
+ * exclude=true|false option controls whether or not we return
+ * threads that only match in an excluded message */
+ const char **search_exclude_tags;
+ size_t search_exclude_tags_length;
+ unsigned int i;
+
+ 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]);
+
+ if (exclude == EXCLUDE_FALSE) {
+ notmuch_query_set_omit_excluded (query, FALSE);
+ params.omit_excluded = FALSE;
+ }
+
ret = do_show (ctx, query, format, &params);
+ }
notmuch_query_destroy (query);
- notmuch_database_close (notmuch);
+ notmuch_database_destroy (notmuch);
if (params.cryptoctx)
g_object_unref(params.cryptoctx);
diff --git a/notmuch-tag.c b/notmuch-tag.c
index 36b9b092..7d186399 100644
--- a/notmuch-tag.c
+++ b/notmuch-tag.c
@@ -53,10 +53,14 @@ _escape_tag (char *buf, const char *tag)
return buf;
}
+typedef struct {
+ const char *tag;
+ notmuch_bool_t remove;
+} tag_operation_t;
+
static char *
-_optimize_tag_query (void *ctx, const char *orig_query_string, char *argv[],
- int *add_tags, int add_tags_count,
- int *remove_tags, int remove_tags_count)
+_optimize_tag_query (void *ctx, const char *orig_query_string,
+ const tag_operation_t *tag_ops)
{
/* This is subtler than it looks. Xapian ignores the '-' operator
* at the beginning both queries and parenthesized groups and,
@@ -71,15 +75,16 @@ _optimize_tag_query (void *ctx, const char *orig_query_string, char *argv[],
int i;
unsigned int max_tag_len = 0;
+ /* Don't optimize if there are no tag changes. */
+ if (tag_ops[0].tag == NULL)
+ return talloc_strdup (ctx, orig_query_string);
+
/* Allocate a buffer for escaping tags. This is large enough to
* hold a fully escaped tag with every character doubled plus
* enclosing quotes and a NUL. */
- for (i = 0; i < add_tags_count; i++)
- if (strlen (argv[add_tags[i]] + 1) > max_tag_len)
- max_tag_len = strlen (argv[add_tags[i]] + 1);
- for (i = 0; i < remove_tags_count; i++)
- if (strlen (argv[remove_tags[i]] + 1) > max_tag_len)
- max_tag_len = strlen (argv[remove_tags[i]] + 1);
+ for (i = 0; tag_ops[i].tag; i++)
+ if (strlen (tag_ops[i].tag) > max_tag_len)
+ max_tag_len = strlen (tag_ops[i].tag);
escaped = talloc_array(ctx, char, max_tag_len * 2 + 3);
if (!escaped)
return NULL;
@@ -90,16 +95,11 @@ _optimize_tag_query (void *ctx, const char *orig_query_string, char *argv[],
else
query_string = talloc_asprintf (ctx, "( %s ) and (", orig_query_string);
- for (i = 0; i < add_tags_count && query_string; i++) {
- query_string = talloc_asprintf_append_buffer (
- query_string, "%snot tag:%s", join,
- _escape_tag (escaped, argv[add_tags[i]] + 1));
- join = " or ";
- }
- for (i = 0; i < remove_tags_count && query_string; i++) {
+ for (i = 0; tag_ops[i].tag && query_string; i++) {
query_string = talloc_asprintf_append_buffer (
- query_string, "%stag:%s", join,
- _escape_tag (escaped, argv[remove_tags[i]] + 1));
+ query_string, "%s%stag:%s", join,
+ tag_ops[i].remove ? "" : "not ",
+ _escape_tag (escaped, tag_ops[i].tag));
join = " or ";
}
@@ -110,21 +110,75 @@ _optimize_tag_query (void *ctx, const char *orig_query_string, char *argv[],
return query_string;
}
+/* Tag messages matching 'query_string' according to 'tag_ops', which
+ * must be an array of tagging operations terminated with an empty
+ * element. */
+static int
+tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string,
+ tag_operation_t *tag_ops, notmuch_bool_t synchronize_flags)
+{
+ notmuch_query_t *query;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ int i;
+
+ /* Optimize the query so it excludes messages that already have
+ * the specified set of tags. */
+ query_string = _optimize_tag_query (ctx, query_string, tag_ops);
+ if (query_string == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+
+ query = notmuch_query_create (notmuch, query_string);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+
+ /* tagging is not interested in any special sort order */
+ notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_valid (messages) && !interrupted;
+ notmuch_messages_move_to_next (messages))
+ {
+ message = notmuch_messages_get (messages);
+
+ notmuch_message_freeze (message);
+
+ for (i = 0; tag_ops[i].tag; i++) {
+ if (tag_ops[i].remove)
+ notmuch_message_remove_tag (message, tag_ops[i].tag);
+ else
+ notmuch_message_add_tag (message, tag_ops[i].tag);
+ }
+
+ notmuch_message_thaw (message);
+
+ if (synchronize_flags)
+ notmuch_message_tags_to_maildir_flags (message);
+
+ notmuch_message_destroy (message);
+ }
+
+ notmuch_query_destroy (query);
+
+ return interrupted;
+}
+
int
notmuch_tag_command (void *ctx, int argc, char *argv[])
{
- int *add_tags, *remove_tags;
- int add_tags_count = 0;
- int remove_tags_count = 0;
+ tag_operation_t *tag_ops;
+ int tag_ops_count = 0;
char *query_string;
notmuch_config_t *config;
notmuch_database_t *notmuch;
- notmuch_query_t *query;
- notmuch_messages_t *messages;
- notmuch_message_t *message;
struct sigaction action;
notmuch_bool_t synchronize_flags;
int i;
+ int ret;
/* Setup our handler for SIGINT */
memset (&action, 0, sizeof (struct sigaction));
@@ -133,35 +187,33 @@ notmuch_tag_command (void *ctx, int argc, char *argv[])
action.sa_flags = SA_RESTART;
sigaction (SIGINT, &action, NULL);
- add_tags = talloc_size (ctx, argc * sizeof (int));
- if (add_tags == NULL) {
- fprintf (stderr, "Out of memory.\n");
- return 1;
- }
+ argc--; argv++; /* skip subcommand argument */
- remove_tags = talloc_size (ctx, argc * sizeof (int));
- if (remove_tags == NULL) {
+ /* Array of tagging operations (add or remove), terminated with an
+ * empty element. */
+ tag_ops = talloc_array (ctx, tag_operation_t, argc + 1);
+ if (tag_ops == NULL) {
fprintf (stderr, "Out of memory.\n");
return 1;
}
- argc--; argv++; /* skip subcommand argument */
-
for (i = 0; i < argc; i++) {
if (strcmp (argv[i], "--") == 0) {
i++;
break;
}
- if (argv[i][0] == '+') {
- add_tags[add_tags_count++] = i;
- } else if (argv[i][0] == '-') {
- remove_tags[remove_tags_count++] = i;
+ if (argv[i][0] == '+' || argv[i][0] == '-') {
+ tag_ops[tag_ops_count].tag = argv[i] + 1;
+ tag_ops[tag_ops_count].remove = (argv[i][0] == '-');
+ tag_ops_count++;
} else {
break;
}
}
- if (add_tags_count == 0 && remove_tags_count == 0) {
+ tag_ops[tag_ops_count].tag = NULL;
+
+ if (tag_ops_count == 0) {
fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
return 1;
}
@@ -173,61 +225,19 @@ notmuch_tag_command (void *ctx, int argc, char *argv[])
return 1;
}
- /* Optimize the query so it excludes messages that already have
- * the specified set of tags. */
- query_string = _optimize_tag_query (ctx, query_string, argv,
- add_tags, add_tags_count,
- remove_tags, remove_tags_count);
- if (query_string == NULL) {
- fprintf (stderr, "Out of memory.\n");
- return 1;
- }
-
config = notmuch_config_open (ctx, NULL, NULL);
if (config == NULL)
return 1;
- notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_WRITE);
- if (notmuch == NULL)
+ if (notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
return 1;
synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
- query = notmuch_query_create (notmuch, query_string);
- if (query == NULL) {
- fprintf (stderr, "Out of memory.\n");
- return 1;
- }
+ ret = tag_query (ctx, notmuch, query_string, tag_ops, synchronize_flags);
- /* tagging is not interested in any special sort order */
- notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
-
- for (messages = notmuch_query_search_messages (query);
- notmuch_messages_valid (messages) && !interrupted;
- notmuch_messages_move_to_next (messages))
- {
- message = notmuch_messages_get (messages);
-
- notmuch_message_freeze (message);
-
- for (i = 0; i < remove_tags_count; i++)
- notmuch_message_remove_tag (message,
- argv[remove_tags[i]] + 1);
-
- for (i = 0; i < add_tags_count; i++)
- notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
+ notmuch_database_destroy (notmuch);
- notmuch_message_thaw (message);
-
- if (synchronize_flags)
- notmuch_message_tags_to_maildir_flags (message);
-
- notmuch_message_destroy (message);
- }
-
- notmuch_query_destroy (query);
- notmuch_database_close (notmuch);
-
- return interrupted;
+ return ret;
}
diff --git a/show-message.c b/show-message.c
deleted file mode 100644
index 83ecf813..00000000
--- a/show-message.c
+++ /dev/null
@@ -1,106 +0,0 @@
-/* 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>
- */
-
-#include "notmuch-client.h"
-
-typedef struct show_message_state {
- int part_count;
-} show_message_state_t;
-
-static void
-show_message_part (mime_node_t *node,
- show_message_state_t *state,
- const notmuch_show_format_t *format,
- int first)
-{
- /* Formatters expect the envelope for embedded message parts */
- GMimeObject *part = node->envelope_part ?
- GMIME_OBJECT (node->envelope_part) : node->part;
- int i;
-
- if (!first)
- fputs (format->part_sep, stdout);
-
- /* Format this part */
- if (format->part_start)
- format->part_start (part, &(state->part_count));
-
- if (node->decrypt_attempted && format->part_encstatus)
- format->part_encstatus (node->decrypt_success);
-
- if (node->verify_attempted && format->part_sigstatus)
-#ifdef GMIME_ATLEAST_26
- format->part_sigstatus (node->sig_list);
-#else
- format->part_sigstatus (node->sig_validity);
-#endif
-
- format->part_content (part);
-
- if (node->envelope_part) {
- fputs (format->header_start, stdout);
- if (format->header_message_part)
- format->header_message_part (GMIME_MESSAGE (node->part));
- fputs (format->header_end, stdout);
-
- fputs (format->body_start, stdout);
- }
-
- /* Recurse over the children */
- state->part_count += 1;
- for (i = 0; i < node->nchildren; i++)
- show_message_part (mime_node_child (node, i), state, format, i == 0);
-
- /* Finish this part */
- if (node->envelope_part)
- fputs (format->body_end, stdout);
-
- if (format->part_end)
- format->part_end (part);
-}
-
-notmuch_status_t
-show_message_body (notmuch_message_t *message,
- const notmuch_show_format_t *format,
- notmuch_show_params_t *params)
-{
- notmuch_status_t ret;
- show_message_state_t state;
- mime_node_t *root, *part;
-
- ret = mime_node_open (NULL, message, params->cryptoctx, params->decrypt,
- &root);
- if (ret)
- return ret;
-
- /* The caller of show_message_body has already handled the
- * outermost envelope, so skip it. */
- state.part_count = MAX (params->part, 1);
-
- part = mime_node_seek_dfs (root, state.part_count);
- if (part)
- show_message_part (part, &state, format, TRUE);
-
- talloc_free (root);
-
- return NOTMUCH_STATUS_SUCCESS;
-}
diff --git a/test/config b/test/config
new file mode 100755
index 00000000..93ecb139
--- /dev/null
+++ b/test/config
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+
+test_description='"notmuch config"'
+. test-lib.sh
+
+test_begin_subtest "Get string value"
+test_expect_equal "$(notmuch config get user.name)" "Notmuch Test Suite"
+
+test_begin_subtest "Get list value"
+test_expect_equal "$(notmuch config get new.tags)" "\
+unread
+inbox"
+
+test_begin_subtest "Set string value"
+notmuch config set foo.string "this is a string value"
+test_expect_equal "$(notmuch config get foo.string)" "this is a string value"
+
+test_begin_subtest "Set string value again"
+notmuch config set foo.string "this is another string value"
+test_expect_equal "$(notmuch config get foo.string)" "this is another string value"
+
+test_begin_subtest "Set list value"
+notmuch config set foo.list this "is a" "list value"
+test_expect_equal "$(notmuch config get foo.list)" "\
+this
+is a
+list value"
+
+test_begin_subtest "Set list value again"
+notmuch config set foo.list this "is another" "list value"
+test_expect_equal "$(notmuch config get foo.list)" "\
+this
+is another
+list value"
+
+test_begin_subtest "Remove key"
+notmuch config set foo.remove baz
+notmuch config set foo.remove
+test_expect_equal "$(notmuch config get foo.remove)" ""
+
+test_begin_subtest "Remove non-existent key"
+notmuch config set foo.nonexistent
+test_expect_equal "$(notmuch config get foo.nonexistent)" ""
+
+test_begin_subtest "List all items"
+notmuch config set database.path "/canonical/path"
+output=$(notmuch config list)
+test_expect_equal "$output" "\
+database.path=/canonical/path
+user.name=Notmuch Test Suite
+user.primary_email=test_suite@notmuchmail.org
+user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org
+new.tags=unread;inbox;
+new.ignore=
+search.exclude_tags=
+maildir.synchronize_flags=true
+foo.string=this is another string value
+foo.list=this;is another;list value;"
+
+test_done
diff --git a/test/crypto b/test/crypto
index 6723ef87..be752b19 100755
--- a/test/crypto
+++ b/test/crypto
@@ -43,6 +43,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
expected='[[[{"id": "XXXXX",
"match": true,
+ "excluded": false,
"filename": "YYYYY",
"timestamp": 946728000,
"date_relative": "2000-01-01",
@@ -50,9 +51,8 @@ expected='[[[{"id": "XXXXX",
"headers": {"Subject": "test signed message 001",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
"body": [{"id": 1,
"sigstatus": [{"status": "good",
"fingerprint": "'$FINGERPRINT'",
@@ -77,6 +77,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
expected='[[[{"id": "XXXXX",
"match": true,
+ "excluded": false,
"filename": "YYYYY",
"timestamp": 946728000,
"date_relative": "2000-01-01",
@@ -84,9 +85,8 @@ expected='[[[{"id": "XXXXX",
"headers": {"Subject": "test signed message 001",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
"body": [{"id": 1,
"sigstatus": [{"status": "good",
"fingerprint": "'$FINGERPRINT'",
@@ -111,6 +111,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
expected='[[[{"id": "XXXXX",
"match": true,
+ "excluded": false,
"filename": "YYYYY",
"timestamp": 946728000,
"date_relative": "2000-01-01",
@@ -118,9 +119,8 @@ expected='[[[{"id": "XXXXX",
"headers": {"Subject": "test signed message 001",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
"body": [{"id": 1,
"sigstatus": [{"status": "error",
"keyid": "'$(echo $FINGERPRINT | cut -c 25-)'",
@@ -151,7 +151,7 @@ test_begin_subtest "decryption, --format=text"
output=$(notmuch show --format=text --decrypt subject:"test encrypted message 001" \
| notmuch_show_sanitize_all \
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
-expected=' message{ id:XXXXX depth:0 match:1 filename:XXXXX
+expected=' message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2000-01-01) (encrypted inbox)
Subject: test encrypted message 001
@@ -185,6 +185,7 @@ output=$(notmuch show --format=json --decrypt subject:"test encrypted message 00
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
expected='[[[{"id": "XXXXX",
"match": true,
+ "excluded": false,
"filename": "YYYYY",
"timestamp": 946728000,
"date_relative": "2000-01-01",
@@ -192,9 +193,8 @@ expected='[[[{"id": "XXXXX",
"headers": {"Subject": "test encrypted message 001",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
"body": [{"id": 1,
"encstatus": [{"status": "good"}],
"sigstatus": [],
@@ -240,6 +240,7 @@ output=$(notmuch show --format=json --decrypt subject:"test encrypted message 00
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
expected='[[[{"id": "XXXXX",
"match": true,
+ "excluded": false,
"filename": "YYYYY",
"timestamp": 946728000,
"date_relative": "2000-01-01",
@@ -247,9 +248,8 @@ expected='[[[{"id": "XXXXX",
"headers": {"Subject": "test encrypted message 001",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
"body": [{"id": 1,
"encstatus": [{"status": "bad"}],
"content-type": "multipart/encrypted",
@@ -275,6 +275,7 @@ output=$(notmuch show --format=json --decrypt subject:"test encrypted message 00
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
expected='[[[{"id": "XXXXX",
"match": true,
+ "excluded": false,
"filename": "YYYYY",
"timestamp": 946728000,
"date_relative": "2000-01-01",
@@ -282,9 +283,8 @@ expected='[[[{"id": "XXXXX",
"headers": {"Subject": "test encrypted message 002",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
"body": [{"id": 1,
"encstatus": [{"status": "good"}],
"sigstatus": [{"status": "good",
@@ -330,6 +330,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
| sed -e 's|"created": [1234567890]*|"created": 946728000|')
expected='[[[{"id": "XXXXX",
"match": true,
+ "excluded": false,
"filename": "YYYYY",
"timestamp": 946728000,
"date_relative": "2000-01-01",
@@ -337,9 +338,8 @@ expected='[[[{"id": "XXXXX",
"headers": {"Subject": "test signed message 001",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
"body": [{"id": 1,
"sigstatus": [{"status": "error",
"keyid": "6D92612D94E46381",
diff --git a/test/emacs b/test/emacs
index 75498928..e9f954c3 100755
--- a/test/emacs
+++ b/test/emacs
@@ -39,7 +39,7 @@ test_begin_subtest "Navigation of notmuch-hello to search results"
test_emacs '(notmuch-hello)
(goto-char (point-min))
(re-search-forward "inbox")
- (widget-button-press (point))
+ (widget-button-press (1- (point)))
(notmuch-test-wait)
(test-output)'
test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-view-inbox
@@ -78,7 +78,7 @@ thread=$(notmuch search --output=threads subject:message-with-invalid-from)
test_emacs "(notmuch-show \"$thread\")
(test-output)"
cat <<EOF >EXPECTED
-Invalid " From <test_suite@notmuchmail.org> (2001-01-05) (inbox)
+"Invalid " (2001-01-05) (inbox)
Subject: message-with-invalid-from
To: Notmuch Test Suite <test_suite@notmuchmail.org>
Date: Fri, 05 Jan 2001 15:43:57 +0000
@@ -139,6 +139,17 @@ test_emacs '(notmuch-search "id:\"123..456@example\"")
output=$(notmuch search 'id:"123..456@example"' | notmuch_search_sanitize)
test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Message with .. in Message-Id (inbox search-add show-add)"
+test_begin_subtest "Message with quote in Message-Id:"
+add_message '[id]="\"quote\"@example"' '[subject]="Message with quote in Message-Id"'
+test_emacs '(notmuch-search "subject:\"Message with quote\"")
+ (notmuch-test-wait)
+ (execute-kbd-macro "+search-add")
+ (notmuch-search-show-thread)
+ (notmuch-test-wait)
+ (execute-kbd-macro "+show-add")'
+output=$(notmuch search 'id:"""quote""@example"' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Message with quote in Message-Id (inbox search-add show-add)"
+
test_begin_subtest "Sending a message via (fake) SMTP"
emacs_deliver_message \
'Testing message sent via SMTP' \
@@ -256,39 +267,227 @@ EOF
test_expect_equal_file OUTPUT EXPECTED
test_begin_subtest "Reply within emacs"
-test_emacs '(notmuch-search "subject:\"testing message sent via SMTP\"")
+test_emacs '(let ((message-hidden-headers ''()))
+ (notmuch-search "subject:\"testing message sent via SMTP\"")
(notmuch-test-wait)
(notmuch-search-reply-to-thread)
- (test-output)'
+ (test-output))'
sed -i -e 's/^In-Reply-To: <.*>$/In-Reply-To: <XXX>/' OUTPUT
+sed -i -e 's/^References: <.*>$/References: <XXX>/' OUTPUT
+sed -i -e 's,^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' OUTPUT
cat <<EOF >EXPECTED
From: Notmuch Test Suite <test_suite@notmuchmail.org>
To: user@example.com
Subject: Re: Testing message sent via SMTP
In-Reply-To: <XXX>
Fcc: ${MAIL_DIR}/sent
+References: <XXX>
+User-Agent: Notmuch/XXX Emacs/XXX
--text follows this line--
-On 01 Jan 2000 12:00:00 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
> This is a test that messages are sent via SMTP
EOF
test_expect_equal_file OUTPUT EXPECTED
+test_begin_subtest "Reply from alternate address within emacs"
+add_message '[from]="Sender <sender@example.com>"' \
+ [to]=test_suite_other@notmuchmail.org
+
+test_emacs "(let ((message-hidden-headers '()))
+ (notmuch-search \"id:\\\"${gen_msg_id}\\\"\")
+ (notmuch-test-wait)
+ (notmuch-search-reply-to-thread)
+ (test-output))"
+sed -i -e 's,^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+To: Sender <sender@example.com>
+Subject: Re: ${test_subtest_name}
+In-Reply-To: <${gen_msg_id}>
+Fcc: ${MAIL_DIR}/sent
+References: <${gen_msg_id}>
+User-Agent: Notmuch/XXX Emacs/XXX
+--text follows this line--
+Sender <sender@example.com> writes:
+
+> This is just a test message (#${gen_msg_cnt})
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "Reply from address in named group list within emacs"
+add_message '[from]="Sender <sender@example.com>"' \
+ '[to]=group:test_suite@notmuchmail.org,someone@example.com\;' \
+ [cc]=test_suite_other@notmuchmail.org
+
+test_emacs "(let ((message-hidden-headers '()))
+ (notmuch-search \"id:\\\"${gen_msg_id}\\\"\")
+ (notmuch-test-wait)
+ (notmuch-search-reply-to-thread)
+ (test-output))"
+sed -i -e 's,^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Sender <sender@example.com>, someone@example.com
+Subject: Re: ${test_subtest_name}
+In-Reply-To: <${gen_msg_id}>
+Fcc: ${MAIL_DIR}/sent
+References: <${gen_msg_id}>
+User-Agent: Notmuch/XXX Emacs/XXX
+--text follows this line--
+Sender <sender@example.com> writes:
+
+> This is just a test message (#${gen_msg_cnt})
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "Reply within emacs to a multipart/mixed message"
+test_emacs '(let ((message-hidden-headers ''()))
+ (notmuch-show "id:20091118002059.067214ed@hikari")
+ (notmuch-show-reply)
+ (test-output))'
+sed -i -e 's,^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Adrian Perez de Castro <aperez@igalia.com>, notmuch@notmuchmail.org
+Subject: Re: [notmuch] Introducing myself
+In-Reply-To: <20091118002059.067214ed@hikari>
+Fcc: ${MAIL_DIR}/sent
+References: <20091118002059.067214ed@hikari>
+User-Agent: Notmuch/XXX Emacs/XXX
+--text follows this line--
+Adrian Perez de Castro <aperez@igalia.com> writes:
+
+> Hello to all,
+>
+> I have just heard about Not Much today in some random Linux-related news
+> site (LWN?), my name is Adrian Perez and I work as systems administrator
+> (although I can do some code as well :P). I have always thought that the
+> ideas behind Sup were great, but after some time using it, I got tired of
+> the oddities that it has. I also do not like doing things like having to
+> install Ruby just for reading and sorting mails. Some time ago I thought
+> about doing something like Not Much and in fact I played a bit with the
+> Python+Xapian and the Python+Whoosh combinations, because I find relaxing
+> to code things in Python when I am not working and also it is installed
+> by default on most distribution. I got to have some mailboxes indexed and
+> basic searching working a couple of months ago. Lately I have been very
+> busy and had no time for coding, and them... boom! Not Much appears -- and
+> it is almost exactly what I was trying to do, but faster. I have been
+> playing a bit with Not Much today, and I think it has potential.
+>
+> Also, I would like to share one idea I had in mind, that you might find
+> interesting: One thing I have found very annoying is having to re-tag my
+> mail when the indexes get b0rked (it happened a couple of times to me while
+> using Sup), so I was planning to mails as read/unread and adding the tags
+> not just to the index, but to the mail text itself, e.g. by adding a
+> "X-Tags" header field or by reusing the "Keywords" one. This way, the index
+> could be totally recreated by re-reading the mail directories, and this
+> would also allow to a tools like OfflineIMAP [1] to get the mails into a
+> local maildir, tagging and indexing the mails with the e-mail reader and
+> then syncing back the messages with the "X-Tags" header to the IMAP server.
+> This would allow to use the mail reader from a different computer and still
+> have everything tagged finely.
+>
+> Best regards,
+>
+>
+> ---
+> [1] http://software.complete.org/software/projects/show/offlineimap
+>
+> --
+> Adrian Perez de Castro <aperez@igalia.com>
+> Igalia - Free Software Engineering
+> _______________________________________________
+> notmuch mailing list
+> notmuch@notmuchmail.org
+> http://notmuchmail.org/mailman/listinfo/notmuch
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "Reply within emacs to a multipart/alternative message"
+test_emacs '(let ((message-hidden-headers ''()))
+ (notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
+ (notmuch-show-reply)
+ (test-output))'
+sed -i -e 's,^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Alex Botero-Lowry <alex.boterolowry@gmail.com>, notmuch@notmuchmail.org
+Subject: Re: [notmuch] preliminary FreeBSD support
+In-Reply-To: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+Fcc: ${MAIL_DIR}/sent
+References: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+User-Agent: Notmuch/XXX Emacs/XXX
+--text follows this line--
+Alex Botero-Lowry <alex.boterolowry@gmail.com> writes:
+
+> I saw the announcement this morning, and was very excited, as I had been
+> hoping sup would be turned into a library,
+> since I like the concept more than the UI (I'd rather an emacs interface).
+>
+> I did a preliminary compile which worked out fine, but
+> sysconf(_SC_SC_GETPW_R_SIZE_MAX) returns -1 on
+> FreeBSD, so notmuch_config_open segfaulted.
+>
+> Attached is a patch that supplies a default buffer size of 64 in cases where
+> -1 is returned.
+>
+> http://www.opengroup.org/austin/docs/austin_328.txt - seems to indicate this
+> is acceptable behavior,
+> and http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.htmlspecifically
+> uses 64 as the
+> buffer size.
+> _______________________________________________
+> notmuch mailing list
+> notmuch@notmuchmail.org
+> http://notmuchmail.org/mailman/listinfo/notmuch
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "Reply within emacs to an html-only message"
+add_message '[content-type]="text/html"' \
+ '[body]="Hi,<br />This is an <b>HTML</b> test message.<br /><br />OK?"'
+test_emacs "(let ((message-hidden-headers '()) (mm-text-html-renderer 'html2text))
+ (notmuch-show \"id:${gen_msg_id}\")
+ (notmuch-show-reply)
+ (test-output))"
+sed -i -e 's,^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To:
+Subject: Re: Reply within emacs to an html-only message
+In-Reply-To: <${gen_msg_id}>
+Fcc: ${MAIL_DIR}/sent
+References: <${gen_msg_id}>
+User-Agent: Notmuch/XXX Emacs/XXX
+--text follows this line--
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
+> Hi,This is an HTML test message.OK?
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
test_begin_subtest "Quote MML tags in reply"
message_id='test-emacs-mml-quoting@message.id'
add_message [id]="$message_id" \
"[subject]='$test_subtest_name'" \
'[body]="<#part disposition=inline>"'
-test_emacs "(notmuch-show \"id:$message_id\")
+test_emacs "(let ((message-hidden-headers '()))
+ (notmuch-show \"id:$message_id\")
(notmuch-show-reply)
- (test-output)"
+ (test-output))"
+sed -i -e 's,^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' OUTPUT
cat <<EOF >EXPECTED
From: Notmuch Test Suite <test_suite@notmuchmail.org>
To:
Subject: Re: Quote MML tags in reply
In-Reply-To: <test-emacs-mml-quoting@message.id>
Fcc: ${MAIL_DIR}/sent
+References: <test-emacs-mml-quoting@message.id>
+User-Agent: Notmuch/XXX Emacs/XXX
--text follows this line--
-On Fri, 05 Jan 2001 15:43:57 +0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
> <#!part disposition=inline>
EOF
test_expect_equal_file OUTPUT EXPECTED
@@ -414,7 +613,7 @@ test_emacs '(notmuch-show "id:\"bought\"")
(reverse-region (point-min) (point-max))
(test-output)'
cat <<EOF >EXPECTED
-Sat, 01 Jan 2000 12:00:00 -0000
+Sat, 01 Jan 2000 12:00:00 +0000
Some One <someone@somewhere.org>
Some One Else <notsomeone@somewhere.org>
Notmuch <notmuch@notmuchmail.org>
diff --git a/test/emacs-hello b/test/emacs-hello
new file mode 100755
index 00000000..a998dc4f
--- /dev/null
+++ b/test/emacs-hello
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+
+test_description="Testing emacs notmuch-hello view"
+. test-lib.sh
+
+EXPECTED=$TEST_DIRECTORY/emacs.expected-output
+
+add_email_corpus
+
+test_begin_subtest "User-defined section with inbox tag"
+test_emacs "(let ((notmuch-hello-sections
+ (list (lambda () (notmuch-hello-insert-searches
+ \"Test\" '((\"inbox\" . \"tag:inbox\")))))))
+ (notmuch-hello)
+ (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-new-section
+
+test_begin_subtest "User-defined section with empty, hidden entry"
+test_emacs "(let ((notmuch-hello-sections
+ (list (lambda () (notmuch-hello-insert-searches
+ \"Test-with-empty\"
+ '((\"inbox\" . \"tag:inbox\")
+ (\"doesnotexist\" . \"tag:doesnotexist\"))
+ :hide-empty-searches t)))))
+ (notmuch-hello)
+ (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-section-with-empty
+
+test_begin_subtest "User-defined section, unread tag filtered out"
+test_emacs "(let ((notmuch-hello-sections
+ (list (lambda () (notmuch-hello-insert-tags-section
+ \"Test-with-filtered\"
+ :hide-tags '(\"unread\"))))))
+ (notmuch-hello)
+ (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-section-hidden-tag
+
+test_begin_subtest "User-defined section, different query for counts"
+test_emacs "(let ((notmuch-hello-sections
+ (list (lambda () (notmuch-hello-insert-tags-section
+ \"Test-with-counts\"
+ :filter-count \"tag:signed\")))))
+ (notmuch-hello)
+ (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-section-counts
+
+test_begin_subtest "Empty custom tags section"
+test_emacs "(let* ((widget (widget-create 'notmuch-hello-tags-section))
+ (notmuch-hello-sections (list (widget-value widget))))
+ (notmuch-hello)
+ (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-empty-custom-tags-section
+
+test_begin_subtest "Empty custom queries section"
+test_emacs "(let* ((widget (widget-create 'notmuch-hello-query-section))
+ (notmuch-hello-sections (list (widget-value widget))))
+ (notmuch-hello)
+ (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-empty-custom-queries-section
+
+test_begin_subtest "Column alignment for tag/queries with long names"
+tag=a-very-long-tag # length carefully calculated for 80 characters window width
+notmuch tag +$tag '*'
+test_emacs '(notmuch-hello)
+ (test-output)'
+notmuch tag -$tag '*'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-long-names
+
+test_done
diff --git a/test/emacs.expected-output/notmuch-hello b/test/emacs.expected-output/notmuch-hello
index 3e59595f..2d698917 100644
--- a/test/emacs.expected-output/notmuch-hello
+++ b/test/emacs.expected-output/notmuch-hello
@@ -2,13 +2,14 @@
Saved searches: [edit]
- 52 inbox 52 unread
+ 52 inbox 52 unread
Search: .
-[Show all tags]
+All tags: [show]
Type a search query and hit RET to view matching threads.
Edit saved searches with the `edit' button.
Hit RET or click on a saved search or tag name to view matching threads.
`=' to refresh this screen. `s' to search messages. `q' to quit.
+ Customize this page.
diff --git a/test/emacs.expected-output/notmuch-hello-empty-custom-queries-section b/test/emacs.expected-output/notmuch-hello-empty-custom-queries-section
new file mode 100644
index 00000000..cd0fdf08
--- /dev/null
+++ b/test/emacs.expected-output/notmuch-hello-empty-custom-queries-section
@@ -0,0 +1,3 @@
+: [hide]
+
+
diff --git a/test/emacs.expected-output/notmuch-hello-empty-custom-tags-section b/test/emacs.expected-output/notmuch-hello-empty-custom-tags-section
new file mode 100644
index 00000000..b56fd671
--- /dev/null
+++ b/test/emacs.expected-output/notmuch-hello-empty-custom-tags-section
@@ -0,0 +1,5 @@
+: [hide]
+
+ 4 attachment 7 signed
+ 52 inbox 52 unread
+
diff --git a/test/emacs.expected-output/notmuch-hello-long-names b/test/emacs.expected-output/notmuch-hello-long-names
new file mode 100644
index 00000000..486d0d9a
--- /dev/null
+++ b/test/emacs.expected-output/notmuch-hello-long-names
@@ -0,0 +1,18 @@
+ Welcome to notmuch. You have 52 messages.
+
+Saved searches: [edit]
+
+ 52 inbox 52 unread
+
+Search: .
+
+All tags: [hide]
+
+ 52 a-very-long-tag 52 inbox 52 unread
+ 4 attachment 7 signed
+
+ Type a search query and hit RET to view matching threads.
+ Edit saved searches with the `edit' button.
+ Hit RET or click on a saved search or tag name to view matching threads.
+ `=' to refresh this screen. `s' to search messages. `q' to quit.
+ Customize this page.
diff --git a/test/emacs.expected-output/notmuch-hello-new-section b/test/emacs.expected-output/notmuch-hello-new-section
new file mode 100644
index 00000000..67fdef24
--- /dev/null
+++ b/test/emacs.expected-output/notmuch-hello-new-section
@@ -0,0 +1,4 @@
+Test: [hide]
+
+ 52 inbox
+
diff --git a/test/emacs.expected-output/notmuch-hello-no-saved-searches b/test/emacs.expected-output/notmuch-hello-no-saved-searches
index ef0e5d05..05475b15 100644
--- a/test/emacs.expected-output/notmuch-hello-no-saved-searches
+++ b/test/emacs.expected-output/notmuch-hello-no-saved-searches
@@ -2,9 +2,10 @@
Search: .
-[Show all tags]
+All tags: [show]
Type a search query and hit RET to view matching threads.
Edit saved searches with the `edit' button.
Hit RET or click on a saved search or tag name to view matching threads.
`=' to refresh this screen. `s' to search messages. `q' to quit.
+ Customize this page.
diff --git a/test/emacs.expected-output/notmuch-hello-section-counts b/test/emacs.expected-output/notmuch-hello-section-counts
new file mode 100644
index 00000000..7a9827cf
--- /dev/null
+++ b/test/emacs.expected-output/notmuch-hello-section-counts
@@ -0,0 +1,5 @@
+Test-with-counts: [hide]
+
+ 2 attachment 7 signed
+ 7 inbox 7 unread
+
diff --git a/test/emacs.expected-output/notmuch-hello-section-hidden-tag b/test/emacs.expected-output/notmuch-hello-section-hidden-tag
new file mode 100644
index 00000000..809a1142
--- /dev/null
+++ b/test/emacs.expected-output/notmuch-hello-section-hidden-tag
@@ -0,0 +1,4 @@
+Test-with-filtered: [hide]
+
+ 4 attachment 52 inbox 7 signed
+
diff --git a/test/emacs.expected-output/notmuch-hello-section-with-empty b/test/emacs.expected-output/notmuch-hello-section-with-empty
new file mode 100644
index 00000000..5c673179
--- /dev/null
+++ b/test/emacs.expected-output/notmuch-hello-section-with-empty
@@ -0,0 +1,4 @@
+Test-with-empty: [hide]
+
+ 52 inbox
+
diff --git a/test/emacs.expected-output/notmuch-hello-with-empty b/test/emacs.expected-output/notmuch-hello-with-empty
index 71edba73..854e0c2a 100644
--- a/test/emacs.expected-output/notmuch-hello-with-empty
+++ b/test/emacs.expected-output/notmuch-hello-with-empty
@@ -2,13 +2,14 @@
Saved searches: [edit]
- 52 inbox 52 unread 0 empty
+ 52 inbox 52 unread 0 empty
Search: .
-[Show all tags]
+All tags: [show]
Type a search query and hit RET to view matching threads.
Edit saved searches with the `edit' button.
Hit RET or click on a saved search or tag name to view matching threads.
`=' to refresh this screen. `s' to search messages. `q' to quit.
+ Customize this page.
diff --git a/test/encoding b/test/encoding
index f0d073c5..2e1326eb 100755
--- a/test/encoding
+++ b/test/encoding
@@ -6,10 +6,10 @@ test_begin_subtest "Message with text of unknown charset"
add_message '[content-type]="text/plain; charset=unknown-8bit"' \
"[body]=irrelevant"
output=$(notmuch show id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
-test_expect_equal "$output" " message{ id:msg-001@notmuch-test-suite depth:0 match:1 filename:/XXX/mail/msg-001
+test_expect_equal "$output" " message{ id:msg-001@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-001
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox unread)
-Subject: Test message #1
+Subject: Message with text of unknown charset
From: Notmuch Test Suite <test_suite@notmuchmail.org>
To: Notmuch Test Suite <test_suite@notmuchmail.org>
Date: Fri, 05 Jan 2001 15:43:57 +0000
diff --git a/test/excludes b/test/excludes
new file mode 100755
index 00000000..24d653ea
--- /dev/null
+++ b/test/excludes
@@ -0,0 +1,423 @@
+#!/usr/bin/env bash
+test_description='"notmuch search, count and show" with excludes in several variations'
+. ./test-lib.sh
+
+# Generates a thread consisting of a top level message and 'length'
+# replies. The subject of the top message 'subject: top message"
+# and the subject of the nth reply in the thread is "subject: reply n"
+generate_thread ()
+{
+ local subject="$1"
+ local length="$2"
+ generate_message '[subject]="'"${subject}: top message"'"' '[body]="'"body of top message"'"'
+ parent_id=$gen_msg_id
+ gen_thread_msg_id[0]=$gen_msg_id
+ for i in `seq 1 $length`
+ do
+ generate_message '[subject]="'"${subject}: reply $i"'"' \
+ "[in-reply-to]=\<$parent_id\>" \
+ '[body]="'"body of reply $i"'"'
+ gen_thread_msg_id[$i]=$gen_msg_id
+ parent_id=$gen_msg_id
+ done
+ notmuch new > /dev/null
+ # We cannot retrieve the thread_id until after we have run notmuch new.
+ gen_thread_id=`notmuch search --output=threads id:${gen_thread_msg_id[0]}`
+}
+
+#############################################
+# These are the original search exclude tests.
+
+test_begin_subtest "Search, exclude \"deleted\" messages from search"
+notmuch config set search.exclude_tags deleted
+generate_message '[subject]="Not deleted"'
+not_deleted_id=$gen_msg_id
+generate_message '[subject]="Deleted"'
+notmuch new > /dev/null
+notmuch tag +deleted id:$gen_msg_id
+deleted_id=$gen_msg_id
+output=$(notmuch search subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from message search"
+output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from message search --exclude=false"
+output=$(notmuch search --exclude=false --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id
+id:$deleted_id"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from message search (non-existent exclude-tag)"
+notmuch config set search.exclude_tags deleted non_existent_tag
+output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id"
+notmuch config set search.exclude_tags deleted
+
+test_begin_subtest "Search, exclude \"deleted\" messages from search, overridden"
+output=$(notmuch search subject:deleted and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Deleted (deleted inbox unread)"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from threads"
+add_message '[subject]="Not deleted reply"' '[in-reply-to]="<$gen_msg_id>"'
+output=$(notmuch search subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX 2001-01-05 [1/2] Notmuch Test Suite; Not deleted reply (deleted inbox unread)"
+
+test_begin_subtest "Search, don't exclude \"deleted\" messages when --exclude=flag specified"
+output=$(notmuch search --exclude=flag subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX 2001-01-05 [1/2] Notmuch Test Suite; Not deleted reply (deleted inbox unread)"
+
+test_begin_subtest "Search, don't exclude \"deleted\" messages from search if not configured"
+notmuch config set search.exclude_tags
+output=$(notmuch search subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX 2001-01-05 [2/2] Notmuch Test Suite; Deleted (deleted inbox unread)"
+
+
+########################################################
+# We construct some threads for the tests. We use the tag "test" to
+# indicate which messages we will search for.
+
+# A thread of deleted messages; test matches one of them.
+generate_thread "All messages excluded: single match" 5
+notmuch tag +deleted $gen_thread_id
+notmuch tag +test id:${gen_thread_msg_id[2]}
+
+# A thread of deleted messages; test matches two of them.
+generate_thread "All messages excluded: double match" 5
+notmuch tag +deleted $gen_thread_id
+notmuch tag +test id:${gen_thread_msg_id[2]}
+notmuch tag +test id:${gen_thread_msg_id[4]}
+
+# A thread some messages deleted; test only matches a deleted message.
+generate_thread "Some messages excluded: single excluded match" 5
+notmuch tag +deleted +test id:${gen_thread_msg_id[3]}
+
+# A thread some messages deleted; test only matches a non-deleted message.
+generate_thread "Some messages excluded: single non-excluded match" 5
+notmuch tag +deleted id:${gen_thread_msg_id[2]}
+notmuch tag +test id:${gen_thread_msg_id[4]}
+
+# A thread no messages deleted; test matches a message.
+generate_thread "No messages excluded: single match" 5
+notmuch tag +test id:${gen_thread_msg_id[3]}
+
+# Temporarily remove excludes to get list of matching messages
+notmuch config set search.exclude_tags
+matching_message_ids=( `notmuch search --output=messages tag:test` )
+notmuch config set search.exclude_tags deleted
+
+#########################################
+# Notmuch search tests
+
+test_begin_subtest "Search, default exclusion (thread summary)"
+output=$(notmuch search tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, default exclusion (messages)"
+output=$(notmuch search --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, exclude=true (thread summary)"
+output=$(notmuch search --exclude=true tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, exclude=true (messages)"
+output=$(notmuch search --exclude=true --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, exclude=false (thread summary)"
+output=$(notmuch search --exclude=false tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, exclude=false (messages)"
+output=$(notmuch search --exclude=false --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}
+${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, exclude=flag (thread summary)"
+output=$(notmuch search --exclude=flag tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [0/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [0/6] Notmuch Test Suite; All messages excluded: double match: reply 4 (deleted inbox test unread)
+thread:XXX 2001-01-05 [0/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, exclude=flag (messages)"
+output=$(notmuch search --exclude=flag --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}
+${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, default exclusion: tag in query (thread summary)"
+output=$(notmuch search tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, default exclusion: tag in query (messages)"
+output=$(notmuch search --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+test_begin_subtest "Search, exclude=true: tag in query (thread summary)"
+output=$(notmuch search --exclude=true tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, exclude=true: tag in query (messages)"
+output=$(notmuch search --exclude=true --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+test_begin_subtest "Search, exclude=false: tag in query (thread summary)"
+output=$(notmuch search --exclude=false tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, exclude=false: tag in query (messages)"
+output=$(notmuch search --exclude=false --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+test_begin_subtest "Search, exclude=flag: tag in query (thread summary)"
+output=$(notmuch search --exclude=flag tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, exclude=flag: tag in query (messages)"
+output=$(notmuch search --exclude=flag --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+
+#########################################################
+# Notmuch count tests
+
+test_begin_subtest "Count, default exclusion (messages)"
+output=$(notmuch count tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, default exclusion (threads)"
+output=$(notmuch count --output=threads tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, exclude=true (messages)"
+output=$(notmuch count --exclude=true tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, exclude=true (threads)"
+output=$(notmuch count --output=threads --exclude=true tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, exclude=false (messages)"
+output=$(notmuch count --exclude=false tag:test)
+test_expect_equal "$output" "6"
+
+test_begin_subtest "Count, exclude=false (threads)"
+output=$(notmuch count --output=threads --exclude=false tag:test)
+test_expect_equal "$output" "5"
+
+test_begin_subtest "Count, default exclusion: tag in query (messages)"
+output=$(notmuch count tag:test and tag:deleted)
+test_expect_equal "$output" "4"
+
+test_begin_subtest "Count, default exclusion: tag in query (threads)"
+output=$(notmuch count --output=threads tag:test and tag:deleted)
+test_expect_equal "$output" "3"
+
+test_begin_subtest "Count, exclude=true: tag in query (messages)"
+output=$(notmuch count --exclude=true tag:test and tag:deleted)
+test_expect_equal "$output" "4"
+
+test_begin_subtest "Count, exclude=true: tag in query (threads)"
+output=$(notmuch count --output=threads --exclude=true tag:test and tag:deleted)
+test_expect_equal "$output" "3"
+
+test_begin_subtest "Count, exclude=false: tag in query (messages)"
+output=$(notmuch count --exclude=false tag:test and tag:deleted)
+test_expect_equal "$output" "4"
+
+test_begin_subtest "Count, exclude=false: tag in query (threads)"
+output=$(notmuch count --output=threads --exclude=false tag:test and tag:deleted)
+test_expect_equal "$output" "3"
+
+#############################################################
+# Show tests
+
+test_begin_subtest "Show, default exclusion"
+output=$(notmuch show tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" " message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+ message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3"
+
+test_begin_subtest "Show, default exclusion (entire-thread)"
+output=$(notmuch show --entire-thread tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" " message{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 1
+ message{ id:XXXXX depth:2 match:0 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 2
+ message{ id:XXXXX depth:3 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 3
+ message{ id:XXXXX depth:4 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 5
+ message{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 1
+ message{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 2
+ message{ id:XXXXX depth:3 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3
+ message{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 5"
+
+test_begin_subtest "Show, exclude=true"
+output=$(notmuch show --exclude=true tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" " message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+ message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3"
+
+test_begin_subtest "Show, exclude=true (entire-thread)"
+output=$(notmuch show --entire-thread --exclude=true tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" " message{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 1
+ message{ id:XXXXX depth:2 match:0 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 2
+ message{ id:XXXXX depth:3 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 3
+ message{ id:XXXXX depth:4 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 5
+ message{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 1
+ message{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 2
+ message{ id:XXXXX depth:3 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3
+ message{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 5"
+
+test_begin_subtest "Show, exclude=false"
+output=$(notmuch show --exclude=false tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" " message{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 2
+ message{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 2
+ message{ id:XXXXX depth:1 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 4
+ message{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 3
+ message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+ message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3"
+
+test_begin_subtest "Show, exclude=false (entire-thread)"
+output=$(notmuch show --entire-thread --exclude=false tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" " message{ id:XXXXX depth:0 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 1
+ message{ id:XXXXX depth:2 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 2
+ message{ id:XXXXX depth:3 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 3
+ message{ id:XXXXX depth:4 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 5
+ message{ id:XXXXX depth:0 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 1
+ message{ id:XXXXX depth:2 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 2
+ message{ id:XXXXX depth:3 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 3
+ message{ id:XXXXX depth:4 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 5
+ message{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 1
+ message{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 2
+ message{ id:XXXXX depth:3 match:1 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 3
+ message{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 5
+ message{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 1
+ message{ id:XXXXX depth:2 match:0 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 2
+ message{ id:XXXXX depth:3 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 3
+ message{ id:XXXXX depth:4 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 5
+ message{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: top message
+ message{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 1
+ message{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 2
+ message{ id:XXXXX depth:3 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3
+ message{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 4
+ message{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 5"
+
+
+test_done
diff --git a/test/json b/test/json
index 7df43803..64397886 100755
--- a/test/json
+++ b/test/json
@@ -5,7 +5,7 @@ test_description="--format=json output"
test_begin_subtest "Show message: json"
add_message "[subject]=\"json-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-show-message\""
output=$(notmuch show --format=json "json-show-message")
-test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
+test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
test_begin_subtest "Search message: json"
add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
@@ -22,7 +22,7 @@ test_expect_equal "$output" "[{\"thread\": \"XXX\",
test_begin_subtest "Show message: json, utf-8"
add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
output=$(notmuch show --format=json "jsön-show-méssage")
-test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]"
+test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]"
test_begin_subtest "Show message: json, inline attachment filename"
subject='json-show-inline-attachment-filename'
@@ -35,7 +35,7 @@ emacs_deliver_message \
(insert \"Message-ID: <$id>\n\")"
output=$(notmuch show --format=json "id:$id")
filename=$(notmuch search --output=files "id:$id")
-test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"filename\": \"$filename\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"test_suite@notmuchmail.org\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"filename\": \"README\"}]}]}, []]]]"
+test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\": false, \"filename\": \"$filename\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"test_suite@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"filename\": \"README\"}]}]}, []]]]"
test_begin_subtest "Search message: json, utf-8"
add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
diff --git a/test/maildir-sync b/test/maildir-sync
index d5872a53..d72ec07e 100755
--- a/test/maildir-sync
+++ b/test/maildir-sync
@@ -46,6 +46,7 @@ test_begin_subtest "notmuch show works with renamed file (without notmuch new)"
output=$(notmuch show --format=json id:${gen_msg_id} | filter_show_json)
test_expect_equal "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
"match": true,
+"excluded": false,
"filename": "MAIL_DIR/cur/adding-replied-tag:2,RS",
"timestamp": 978709437,
"date_relative": "2001-01-05",
@@ -53,8 +54,6 @@ test_expect_equal "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
"headers": {"Subject": "Adding replied tag",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
-"Cc": "",
-"Bcc": "",
"Date": "Fri,
05 Jan 2001 15:43:57 +0000"},
"body": [{"id": 1,
diff --git a/test/multipart b/test/multipart
index 2dd73f59..72d39276 100755
--- a/test/multipart
+++ b/test/multipart
@@ -46,6 +46,7 @@ Content-Disposition: inline
EOF
cat embedded_message >> ${MAIL_DIR}/multipart
cat <<EOF >> ${MAIL_DIR}/multipart
+
--=-=-=
Content-Disposition: attachment; filename=attachment
@@ -108,7 +109,7 @@ notmuch new > /dev/null
test_begin_subtest "--format=text --part=0, full message"
notmuch show --format=text --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
cat <<EOF >EXPECTED
- message{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 filename:${MAIL_DIR}/multipart
+ message{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 excluded:0 filename:${MAIL_DIR}/multipart
header{
Carl Worth <cworth@cworth.org> (2001-01-05) (attachment inbox signed unread)
Subject: Multipart message
@@ -322,10 +323,10 @@ notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' | s
echo >>OUTPUT # expect *no* newline at end of output
cat <<EOF >EXPECTED
-{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "match": true, "filename": "${MAIL_DIR}/multipart", "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Cc": "", "Bcc": "", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [
+{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "match": true, "excluded": false, "filename": "${MAIL_DIR}/multipart", "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [
{"id": 1, "content-type": "multipart/signed", "content": [
{"id": 2, "content-type": "multipart/mixed", "content": [
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
{"id": 4, "content-type": "multipart/alternative", "content": [
{"id": 5, "content-type": "text/html"},
{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]},
@@ -342,7 +343,7 @@ cat <<EOF >EXPECTED
{"id": 1, "content-type": "multipart/signed", "content": [
{"id": 2, "content-type": "multipart/mixed", "content": [
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
{"id": 4, "content-type": "multipart/alternative", "content": [
{"id": 5, "content-type": "text/html"},
{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]},
@@ -358,7 +359,7 @@ echo >>OUTPUT # expect *no* newline at end of output
cat <<EOF >EXPECTED
{"id": 2, "content-type": "multipart/mixed", "content": [
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
{"id": 4, "content-type": "multipart/alternative", "content": [
{"id": 5, "content-type": "text/html"},
{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]},
@@ -372,7 +373,7 @@ notmuch show --format=json --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' | s
echo >>OUTPUT # expect *no* newline at end of output
cat <<EOF >EXPECTED
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
{"id": 4, "content-type": "multipart/alternative", "content": [
{"id": 5, "content-type": "text/html"},
{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}
@@ -449,58 +450,80 @@ test_expect_equal_file OUTPUT "${MAIL_DIR}"/multipart
test_begin_subtest "--format=raw --part=1, message body"
notmuch show --format=raw --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
-# output should *not* include newline
-echo >>OUTPUT
-cat <<EOF >EXPECTED
-Subject: html message
-From: Carl Worth <cworth@cworth.org>
-To: cworth@cworth.org
-Date: Fri, 05 Jan 2001 15:42:57 +0000
-
-<p>This is an embedded message, with a multipart/alternative part.</p>
-This is an embedded message, with a multipart/alternative part.
-This is a text attachment.
-And this message is signed.
-
--Carl
------BEGIN PGP SIGNATURE-----
-Version: GnuPG v1.4.11 (GNU/Linux)
-
-iEYEARECAAYFAk3SA/gACgkQ6JDdNq8qSWj0sACghqVJEQJUs3yV8zbTzhgnSIcD
-W6cAmQE4dcYrx/LPLtYLZm1jsGauE5hE
-=zkga
------END PGP SIGNATURE-----
-EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_file OUTPUT "${MAIL_DIR}"/multipart
test_begin_subtest "--format=raw --part=2, multipart/mixed"
notmuch show --format=raw --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
cat <<EOF >EXPECTED
-Subject: html message
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline
+
From: Carl Worth <cworth@cworth.org>
To: cworth@cworth.org
+Subject: html message
Date: Fri, 05 Jan 2001 15:42:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=-=="
+
+--==-=-==
+Content-Type: text/html
<p>This is an embedded message, with a multipart/alternative part.</p>
+
+--==-=-==
+Content-Type: text/plain
+
This is an embedded message, with a multipart/alternative part.
+
+--==-=-==--
+
+--=-=-=
+Content-Disposition: attachment; filename=attachment
+
This is a text attachment.
+
+--=-=-=
+
And this message is signed.
-Carl
+
+--=-=-=--
EOF
test_expect_equal_file OUTPUT EXPECTED
test_begin_subtest "--format=raw --part=3, rfc822 part"
-test_subtest_known_broken
-
notmuch show --format=raw --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
test_expect_equal_file OUTPUT embedded_message
-test_begin_subtest "--format=raw --part=4, rfc822's html part"
+test_begin_subtest "--format=raw --part=4, rfc822's multipart"
notmuch show --format=raw --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
cat <<EOF >EXPECTED
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Subject: html message
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=-=="
+
+--==-=-==
+Content-Type: text/html
+
<p>This is an embedded message, with a multipart/alternative part.</p>
+
+--==-=-==
+Content-Type: text/plain
+
This is an embedded message, with a multipart/alternative part.
+
+--==-=-==--
EOF
test_expect_equal_file OUTPUT EXPECTED
@@ -589,9 +612,61 @@ Non-text part: text/html
EOF
test_expect_equal_file OUTPUT EXPECTED
+test_begin_subtest "'notmuch reply' to a multipart message with json format"
+notmuch reply --format=json 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT
+cat <<EOF >EXPECTED
+{"reply-headers": {"Subject": "Re: Multipart message",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Carl Worth <cworth@cworth.org>,
+ cworth@cworth.org",
+ "In-reply-to": "<87liy5ap00.fsf@yoom.home.cworth.org>",
+ "References": " <87liy5ap00.fsf@yoom.home.cworth.org>"},
+ "original": {"id": "XXXXX",
+ "match": false,
+ "excluded": false,
+ "filename": "YYYYY",
+ "timestamp": 978709437,
+ "date_relative": "2001-01-05",
+ "tags": ["attachment","inbox","signed","unread"],
+ "headers": {"Subject": "Multipart message",
+ "From": "Carl Worth <cworth@cworth.org>",
+ "To": "cworth@cworth.org",
+ "Date": "Fri,
+ 05 Jan 2001 15:43:57 +0000"},
+ "body": [{"id": 1,
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "multipart/mixed",
+ "content": [{"id": 3,
+ "content-type": "message/rfc822",
+ "content": [{"headers": {"Subject": "html message",
+ "From": "Carl Worth <cworth@cworth.org>",
+ "To": "cworth@cworth.org",
+ "Date": "Fri,
+ 05 Jan 2001 15:42:57 +0000"},
+ "body": [{"id": 4,
+ "content-type": "multipart/alternative",
+ "content": [{"id": 5,
+ "content-type": "text/html"},
+ {"id": 6,
+ "content-type": "text/plain",
+ "content": "This is an embedded message,
+ with a multipart/alternative part.\n"}]}]}]},
+ {"id": 7,
+ "content-type": "text/plain",
+ "filename": "YYYYY",
+ "content": "This is a text attachment.\n"},
+ {"id": 8,
+ "content-type": "text/plain",
+ "content": "And this message is signed.\n\n-Carl\n"}]},
+ {"id": 9,
+ "content-type": "application/pgp-signature"}]}]}}
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
test_begin_subtest "'notmuch show --part' does not corrupt a part with CRLF pair"
notmuch show --format=raw --part=3 id:base64-part-with-crlf > crlf.out
echo -n -e "\xEF\x0D\x0A" > crlf.expected
test_expect_equal_file crlf.out crlf.expected
-test_done
+test_done \ No newline at end of file
diff --git a/test/notmuch-test b/test/notmuch-test
index e14d34e4..bfad5d3c 100755
--- a/test/notmuch-test
+++ b/test/notmuch-test
@@ -19,6 +19,7 @@ cd $(dirname "$0")
TESTS="
basic
help-test
+ config
new
count
search
@@ -27,6 +28,7 @@ TESTS="
search-position-overlap-bug
search-insufficient-from-quoting
search-limiting
+ excludes
tagging
json
multipart
@@ -54,6 +56,7 @@ TESTS="
argument-parsing
emacs-test-functions
emacs-address-cleaning
+ emacs-hello
emacs-show
"
TESTS=${NOTMUCH_TESTS:=$TESTS}
diff --git a/test/python b/test/python
index 6018c2d0..3f03a2e3 100755
--- a/test/python
+++ b/test/python
@@ -28,4 +28,12 @@ EOF
notmuch search --sort=oldest-first --output=messages tag:inbox | sed s/^id:// > EXPECTED
test_expect_equal_file OUTPUT EXPECTED
+test_begin_subtest "get non-existent file"
+test_python <<EOF
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+print db.find_message_by_filename("i-dont-exist")
+EOF
+test_expect_equal "$(cat OUTPUT)" "None"
+
test_done
diff --git a/test/raw b/test/raw
index 0171e641..de0b8677 100755
--- a/test/raw
+++ b/test/raw
@@ -3,11 +3,8 @@
test_description='notmuch show --format=raw'
. ./test-lib.sh
-test_begin_subtest "Generate some messages"
-generate_message
-generate_message
-output=$(NOTMUCH_NEW)
-test_expect_equal "$output" "Added 2 new messages to the database."
+add_message
+add_message
test_begin_subtest "Attempt to show multiple raw messages"
output=$(notmuch show --format=raw "*" 2>&1)
diff --git a/test/search b/test/search
index 414be356..a7a0b18d 100755
--- a/test/search
+++ b/test/search
@@ -129,29 +129,4 @@ add_message '[subject]="utf8-message-body-subject"' '[date]="Sat, 01 Jan 2000 12
output=$(notmuch search "bödý" | notmuch_search_sanitize)
test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-message-body-subject (inbox unread)"
-test_begin_subtest "Exclude \"deleted\" messages from search"
-notmuch config set search.exclude_tags = deleted
-generate_message '[subject]="Not deleted"'
-generate_message '[subject]="Deleted"'
-notmuch new > /dev/null
-notmuch tag +deleted id:$gen_msg_id
-output=$(notmuch search subject:deleted | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)"
-
-test_begin_subtest "Exclude \"deleted\" messages from search, overridden"
-output=$(notmuch search subject:deleted and tag:deleted | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Deleted (deleted inbox unread)"
-
-test_begin_subtest "Exclude \"deleted\" messages from threads"
-add_message '[subject]="Not deleted reply"' '[in-reply-to]="<$gen_msg_id>"'
-output=$(notmuch search subject:deleted | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
-thread:XXX 2001-01-05 [1/2] Notmuch Test Suite; Not deleted reply (deleted inbox unread)"
-
-test_begin_subtest "Don't exclude \"deleted\" messages from search if not configured"
-notmuch config set search.exclude_tags
-output=$(notmuch search subject:deleted | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
-thread:XXX 2001-01-05 [2/2] Notmuch Test Suite; Deleted (deleted inbox unread)"
-
test_done
diff --git a/test/search-folder-coherence b/test/search-folder-coherence
index f8119cbb..3f6ec763 100755
--- a/test/search-folder-coherence
+++ b/test/search-folder-coherence
@@ -32,7 +32,7 @@ test_expect_equal_file OUTPUT EXPECTED
test_begin_subtest "Test matches folder:spam"
output=$(notmuch search folder:spam)
-test_expect_equal "$output" "thread:0000000000000001 2001-01-05 [1/1] Notmuch Test Suite; Test message #1 (inbox unread)"
+test_expect_equal "$output" "thread:0000000000000001 2001-01-05 [1/1] Notmuch Test Suite; Single new message (inbox unread)"
test_begin_subtest "Remove folder:spam copy of email"
rm $dir/spam/$(basename $file_x)
diff --git a/test/symbol-test.cc b/test/symbol-test.cc
index 1548ca40..3e96c034 100644
--- a/test/symbol-test.cc
+++ b/test/symbol-test.cc
@@ -4,7 +4,8 @@
int main() {
- (void) notmuch_database_open("fakedb", NOTMUCH_DATABASE_MODE_READ_ONLY);
+ notmuch_database_t *notmuch;
+ notmuch_database_open("fakedb", NOTMUCH_DATABASE_MODE_READ_ONLY, &notmuch);
try {
(void) new Xapian::WritableDatabase("./nonexistant", Xapian::DB_OPEN);
diff --git a/test/tagging b/test/tagging
index 77202bf9..e4782ed4 100755
--- a/test/tagging
+++ b/test/tagging
@@ -38,4 +38,12 @@ test_expect_equal "$output" "\
thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (:\" inbox tag1 unread)
thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 unread)"
+test_begin_subtest "Tagging order"
+notmuch tag +tag4 -tag4 One
+notmuch tag -tag4 +tag4 Two
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (:\" inbox tag1 unread)
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag4 unread)"
+
test_done
diff --git a/test/test-lib.sh b/test/test-lib.sh
index 27815067..06aaea27 100644
--- a/test/test-lib.sh
+++ b/test/test-lib.sh
@@ -318,7 +318,11 @@ generate_message ()
fi
if [ -z "${template[subject]}" ]; then
- template[subject]="Test message #${gen_msg_cnt}"
+ if [ -n "$test_subtest_name" ]; then
+ template[subject]="$test_subtest_name"
+ else
+ template[subject]="Test message #${gen_msg_cnt}"
+ fi
fi
if [ -z "${template[date]}" ]; then
diff --git a/test/thread-naming b/test/thread-naming
index 942e5939..1a1a48f6 100755
--- a/test/thread-naming
+++ b/test/thread-naming
@@ -65,7 +65,7 @@ test_expect_equal "$output" "thread:XXX 2001-01-12 [6/8] Notmuch Test Suite; t
test_begin_subtest 'Test order of messages in "notmuch show"'
output=$(notmuch show thread-naming | notmuch_show_sanitize)
-test_expect_equal "$output" " message{ id:msg-$(printf "%03d" $first)@notmuch-test-suite depth:0 match:1 filename:/XXX/mail/msg-$(printf "%03d" $first)
+test_expect_equal "$output" " message{ id:msg-$(printf "%03d" $first)@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $first)
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (unread)
Subject: thread-naming: Initial thread subject
@@ -79,7 +79,7 @@ This is just a test message (#$first)
part}
body}
message}
- message{ id:msg-$(printf "%03d" $((first + 1)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 1)))
+ message{ id:msg-$(printf "%03d" $((first + 1)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 1)))
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-06) (inbox unread)
Subject: thread-naming: Older changed subject
@@ -93,7 +93,7 @@ This is just a test message (#$((first + 1)))
part}
body}
message}
- message{ id:msg-$(printf "%03d" $((first + 2)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 2)))
+ message{ id:msg-$(printf "%03d" $((first + 2)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 2)))
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-07) (inbox unread)
Subject: thread-naming: Newer changed subject
@@ -107,7 +107,7 @@ This is just a test message (#$((first + 2)))
part}
body}
message}
- message{ id:msg-$(printf "%03d" $((first + 3)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 3)))
+ message{ id:msg-$(printf "%03d" $((first + 3)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 3)))
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-08) (unread)
Subject: thread-naming: Final thread subject
@@ -121,7 +121,7 @@ This is just a test message (#$((first + 3)))
part}
body}
message}
- message{ id:msg-$(printf "%03d" $((first + 4)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 4)))
+ message{ id:msg-$(printf "%03d" $((first + 4)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 4)))
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-09) (inbox unread)
Subject: Re: thread-naming: Initial thread subject
@@ -135,7 +135,7 @@ This is just a test message (#$((first + 4)))
part}
body}
message}
- message{ id:msg-$(printf "%03d" $((first + 5)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 5)))
+ message{ id:msg-$(printf "%03d" $((first + 5)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 5)))
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-10) (inbox unread)
Subject: Aw: thread-naming: Initial thread subject
@@ -149,7 +149,7 @@ This is just a test message (#$((first + 5)))
part}
body}
message}
- message{ id:msg-$(printf "%03d" $((first + 6)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 6)))
+ message{ id:msg-$(printf "%03d" $((first + 6)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 6)))
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-11) (inbox unread)
Subject: Vs: thread-naming: Initial thread subject
@@ -163,7 +163,7 @@ This is just a test message (#$((first + 6)))
part}
body}
message}
- message{ id:msg-$(printf "%03d" $((first + 7)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 7)))
+ message{ id:msg-$(printf "%03d" $((first + 7)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 7)))
header{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-12) (inbox unread)
Subject: Sv: thread-naming: Initial thread subject
diff --git a/version b/version
index c43e1055..9beb74d4 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.12
+0.13.2
diff --git a/vim/Makefile b/vim/Makefile
index 89e18be1..f17bebfe 100644
--- a/vim/Makefile
+++ b/vim/Makefile
@@ -1,11 +1,11 @@
.PHONY: all help install link symlink
-FILES = plugin/notmuch.vim \
- $(wildcard syntax/notmuch-*.vim)
+files = plugin/notmuch.vim \
+ $(wildcard syntax/notmuch-*.vim)
+prefix = $(HOME)/.vim
+destdir = $(prefix)/plugin
-PREFIX = $(shell ls -d ~/.vim/)
-
-OUT_FILES = $(FILES:%=${PREFIX}/%)
+INSTALL = install -D -m644
all: help
@@ -16,9 +16,8 @@ help:
@echo " make install - copy plugin scripts and syntax files to ~/.vim"
@echo " make symlink - create symlinks in ~/.vim (useful for development)"
-install: ${OUT_FILES}
-link symlink:
- ${MAKE} SYMLINK=1 install
+install:
+ @for x in $(files); do $(INSTALL) $(CURDIR)/$$x $(prefix)/$$x; done
-${OUT_FILES}: ${PREFIX}/%: %
- $(if ${SYMLINK},ln -fs,cp) `pwd`/$< $@
+link symlink: INSTALL = ln -fs
+link symlink: install
diff --git a/vim/plugin/notmuch.vim b/vim/plugin/notmuch.vim
index 21985c71..8f27fb92 100644
--- a/vim/plugin/notmuch.vim
+++ b/vim/plugin/notmuch.vim
@@ -48,7 +48,7 @@ let s:notmuch_defaults = {
\ 'g:notmuch_show_part_end_regexp': ' part}' ,
\ 'g:notmuch_show_marker_regexp': ' \\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$',
\
- \ 'g:notmuch_show_message_parse_regexp': '\(id:[^ ]*\) depth:\([0-9]*\) match:\([0-9]*\) filename:\(.*\)$',
+ \ 'g:notmuch_show_message_parse_regexp': '\(id:[^ ]*\) depth:\([0-9]*\) match:\([0-9]*\) excluded:\([0-9]*\) filename:\(.*\)$',
\ 'g:notmuch_show_tags_regexp': '(\([^)]*\))$' ,
\
\ 'g:notmuch_show_signature_regexp': '^\(-- \?\|_\+\)$' ,
@@ -870,7 +870,8 @@ function! s:NM_cmd_show_parse(inlines)
let msg['id'] = m[1]
let msg['depth'] = m[2]
let msg['match'] = m[3]
- let msg['filename'] = m[4]
+ let msg['excluded'] = m[4]
+ let msg['filename'] = m[5]
endif
let in_message = 1