diff options
92 files changed, 2517 insertions, 852 deletions
@@ -1,3 +1,41 @@ +Notmuch 0.16 (2013-MM-DD) +========================= + +Command-Line Interface +---------------------- + +Deprecated commands "part" and "search-tags" are removed. + +Bash command-line completion + + The notmuch command-line completion support for the bash shell has + been rewritten. Supported completions include all the notmuch + commands, command-line arguments, values for keyword arguments, + search prefixes (such as "subject:" or "from:") in all commands that + use search terms, tags after + and - in `notmuch tag`, tags after + "tag:" prefix, user's email addresses after "from:" and "to:" + prefixes, and config options (and some config option values) in + `notmuch config`. The new completion support depends on the + bash-completion package. + +Vim Front-End +------------- + +The vim based front end to notmuch is deprecated and moved to contrib. +We haven't been able to support this as well as we would like, and it +has accumulated bugs and gaps in functionality. We recommend that +people packaging notmuch no longer provide binary packages for +notmuch-vim, but of course that is their decision. + +Emacs Interface +--------------- + +No Emacs 22 support + + The Emacs 22 support added late 2010 was sufficient only for a short + period of time. After being incomplete for roughly 2 years the code + in question was now removed from this release. + Notmuch 0.15.2 (2013-02-17) =========================== @@ -10,10 +48,10 @@ Internal test framework changes ------------------------------- Adjust Emacs test watchdog mechanism to cope with `process-attributes` -being unimplimented. +being unimplemented. Notmuch 0.15.1 (2013-01-24) -========================= +=========================== Internal test framework changes ------------------------------- @@ -205,8 +243,8 @@ Internal test framework changes The emacsclient binary is now user-configurable - The test framework now accepts TEST_EMACSCLIENT in addition to - TEST_EMACS for configuring the emacsclient to use. This is + The test framework now accepts `TEST_EMACSCLIENT` in addition to + `TEST_EMACS` for configuring the emacsclient to use. This is necessary to avoid using an old emacsclient with a new emacs, which can result in buggy behavior. diff --git a/bindings/go/Makefile b/bindings/go/Makefile index c38f2340..1b9e7505 100644 --- a/bindings/go/Makefile +++ b/bindings/go/Makefile @@ -15,8 +15,8 @@ notmuch: .PHONY: goconfig goconfig: - if [ ! -d src/github.com/kless/goconfig/config ]; then \ - $(GO) get github.com/kless/goconfig/config; \ + if [ ! -d github.com/msbranco/goconfig ]; then \ + $(GO) get github.com/msbranco/goconfig; \ fi .PHONY: notmuch-addrlookup diff --git a/bindings/go/src/notmuch-addrlookup/addrlookup.go b/bindings/go/src/notmuch-addrlookup/addrlookup.go index 59283f81..916e5bb2 100644 --- a/bindings/go/src/notmuch-addrlookup/addrlookup.go +++ b/bindings/go/src/notmuch-addrlookup/addrlookup.go @@ -11,7 +11,7 @@ import "sort" // 3rd-party imports import "notmuch" -import "github.com/kless/goconfig/config" +import "github.com/msbranco/goconfig" type mail_addr_freq struct { addr string @@ -178,22 +178,20 @@ type address_matcher struct { } func new_address_matcher() *address_matcher { - var cfg *config.Config - var err error - // honor NOTMUCH_CONFIG home := os.Getenv("NOTMUCH_CONFIG") if home == "" { home = os.Getenv("HOME") } - if cfg, err = config.ReadDefault(path.Join(home, ".notmuch-config")); err != nil { + cfg, err := goconfig.ReadConfigFile(path.Join(home, ".notmuch-config")) + if 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.GetString("database", "path") + primary_email, _ := cfg.GetString("user", "primary_email") + addrbook_tag, err := cfg.GetString("user", "addrbook_tag") if err != nil { addrbook_tag = "addressbook" } diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py index fe692eb7..7ddf5cfe 100644 --- a/bindings/python/notmuch/database.py +++ b/bindings/python/notmuch/database.py @@ -188,7 +188,7 @@ class Database(object): "already has an open one.") db = NotmuchDatabaseP() - status = Database._create(_str(path), Database.MODE.READ_WRITE, byref(db)) + status = Database._create(_str(path), byref(db)) if status != STATUS.SUCCESS: raise NotmuchError(status) diff --git a/bindings/python/notmuch/thread.py b/bindings/python/notmuch/thread.py index 009cb2bf..0454dbd4 100644 --- a/bindings/python/notmuch/thread.py +++ b/bindings/python/notmuch/thread.py @@ -128,11 +128,6 @@ class Thread(object): in the thread. It will only iterate over the messages in the thread which are not replies to other messages in the thread. - To iterate over all messages in the thread, the caller will need to - iterate over the result of :meth:`Message.get_replies` for each - top-level message (and do that recursively for the resulting - messages, etc.). - :returns: :class:`Messages` :raises: :exc:`NotInitializedError` if query is not initialized :raises: :exc:`NullPointerError` if search_messages failed @@ -147,6 +142,28 @@ class Thread(object): return Messages(msgs_p, self) + """notmuch_thread_get_messages""" + _get_messages = nmlib.notmuch_thread_get_messages + _get_messages.argtypes = [NotmuchThreadP] + _get_messages.restype = NotmuchMessagesP + + def get_messages(self): + """Returns a :class:`Messages` iterator for all messages in 'thread' + + :returns: :class:`Messages` + :raises: :exc:`NotInitializedError` if query is not initialized + :raises: :exc:`NullPointerError` if get_messages failed + """ + if not self._thread: + raise NotInitializedError() + + msgs_p = Thread._get_messages(self._thread) + + if not msgs_p: + raise NullPointerError() + + return Messages(msgs_p, self) + _get_matched_messages = nmlib.notmuch_thread_get_matched_messages _get_matched_messages.argtypes = [NotmuchThreadP] _get_matched_messages.restype = c_int diff --git a/bindings/ruby/defs.h b/bindings/ruby/defs.h index fe81b3f9..5b44585a 100644 --- a/bindings/ruby/defs.h +++ b/bindings/ruby/defs.h @@ -262,6 +262,9 @@ VALUE notmuch_rb_thread_get_toplevel_messages (VALUE self); VALUE +notmuch_rb_thread_get_messages (VALUE self); + +VALUE notmuch_rb_thread_get_matched_messages (VALUE self); VALUE diff --git a/bindings/ruby/extconf.rb b/bindings/ruby/extconf.rb index 7b9750f2..6160db26 100644 --- a/bindings/ruby/extconf.rb +++ b/bindings/ruby/extconf.rb @@ -5,9 +5,26 @@ require 'mkmf' -# Notmuch Library -find_header('notmuch.h', '../../lib') -find_library('notmuch', 'notmuch_database_create', '../../lib') +dir = File.join('..', '..', 'lib') + +# includes +$INCFLAGS = "-I#{dir} #{$INCFLAGS}" + +# make sure there are no undefined symbols +$LDFLAGS += ' -Wl,--no-undefined' + +def have_local_library(lib, path, func, headers = nil) + checking_for checking_message(func, lib) do + lib = File.join(path, lib) + if try_func(func, lib, headers) + $LOCAL_LIBS += lib + end + end +end + +if not have_local_library('libnotmuch.so', dir, 'notmuch_database_create', 'notmuch.h') + exit 1 +end # Create Makefile dir_config('notmuch') diff --git a/bindings/ruby/init.c b/bindings/ruby/init.c index f4931d34..663271d4 100644 --- a/bindings/ruby/init.c +++ b/bindings/ruby/init.c @@ -306,6 +306,7 @@ Init_notmuch (void) rb_define_method (notmuch_rb_cThread, "thread_id", notmuch_rb_thread_get_thread_id, 0); /* in thread.c */ rb_define_method (notmuch_rb_cThread, "total_messages", notmuch_rb_thread_get_total_messages, 0); /* in thread.c */ rb_define_method (notmuch_rb_cThread, "toplevel_messages", notmuch_rb_thread_get_toplevel_messages, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "messages", notmuch_rb_thread_get_messages, 0); /* in thread.c */ rb_define_method (notmuch_rb_cThread, "matched_messages", notmuch_rb_thread_get_matched_messages, 0); /* in thread.c */ rb_define_method (notmuch_rb_cThread, "authors", notmuch_rb_thread_get_authors, 0); /* in thread.c */ rb_define_method (notmuch_rb_cThread, "subject", notmuch_rb_thread_get_subject, 0); /* in thread.c */ diff --git a/bindings/ruby/query.c b/bindings/ruby/query.c index e5ba1b7a..1658edee 100644 --- a/bindings/ruby/query.c +++ b/bindings/ruby/query.c @@ -180,5 +180,5 @@ notmuch_rb_query_count_messages (VALUE self) * (function may return 0 after printing a message) * Thus there is nothing we can do here... */ - return UINT2FIX(notmuch_query_count_messages(query)); + return UINT2NUM(notmuch_query_count_messages(query)); } diff --git a/bindings/ruby/thread.c b/bindings/ruby/thread.c index efe5aaf7..56616d9f 100644 --- a/bindings/ruby/thread.c +++ b/bindings/ruby/thread.c @@ -92,6 +92,26 @@ notmuch_rb_thread_get_toplevel_messages (VALUE self) } /* + * call-seq: THREAD.messages => MESSAGES + * + * Get a Notmuch::Messages iterator for the all messages in thread. + */ +VALUE +notmuch_rb_thread_get_messages (VALUE self) +{ + notmuch_messages_t *messages; + notmuch_thread_t *thread; + + Data_Get_Notmuch_Thread (self, thread); + + messages = notmuch_thread_get_messages (thread); + if (!messages) + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); + + return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages); +} + +/* * call-seq: THREAD.matched_messages => fixnum * * Get the number of messages in thread that matched the search diff --git a/completion/README b/completion/README index 40a30e5f..93f0f8bf 100644 --- a/completion/README +++ b/completion/README @@ -3,8 +3,18 @@ notmuch completion This directory contains support for various shells to automatically complete partially entered notmuch command lines. -notmuch-completion.bash Command-line completion for the bash shell +notmuch-completion.bash -notmuch-completion.tcsh Command-line completion for the tcsh shell + Command-line completion for the bash shell. This depends on + bash-completion package [1] version 2.0, which depends on bash + version 3.2 or later. -notmuch-completion.zsh Command-line completion for the zsh shell + [1] http://bash-completion.alioth.debian.org/ + +notmuch-completion.tcsh + + Command-line completion for the tcsh shell. + +notmuch-completion.zsh + + Command-line completion for the zsh shell. diff --git a/completion/notmuch-completion.bash b/completion/notmuch-completion.bash index 8665268c..7bd7745f 100644 --- a/completion/notmuch-completion.bash +++ b/completion/notmuch-completion.bash @@ -1,10 +1,13 @@ -# Bash completion for notmuch +# bash completion for notmuch -*- shell-script -*- # -# Copyright © 2009 Carl Worth +# Copyright © 2013 Jani Nikula +# +# Based on the bash-completion package: +# http://bash-completion.alioth.debian.org/ # # 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 +# the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, @@ -15,57 +18,324 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ . # -# Author: Carl Worth <cworth@cworth.org> -# -# Based on "notmuch help" as follows: -# -# Usage: notmuch <command> [args...] -# -# Where <command> and [args...] are as follows: -# -# setup +# Author: Jani Nikula <jani@nikula.org> # -# new # -# search [options] <search-term> [...] +# BUGS: # -# show <search-terms> +# Add space after an --option without parameter (e.g. reply --decrypt) +# on completion. # -# reply <search-terms> -# -# tag +<tag>|-<tag> [...] [--] <search-terms> [...] -# -# dump [<filename>] -# -# restore <filename> -# -# help [<command>] -_notmuch() +_notmuch_user_emails() { - local current previous commands help_options + notmuch config get user.primary_email + notmuch config get user.other_email +} - previous=${COMP_WORDS[COMP_CWORD-1]} - current="${COMP_WORDS[COMP_CWORD]}" +_notmuch_search_terms() +{ + local cur prev words cword split + # handle search prefixes and tags with colons and equal signs + _init_completion -n := || return - commands="setup new search show reply tag dump restore help" - help_options="setup new search show reply tag dump restore search-terms" - search_options="--max-threads= --first= --sort=" + case "${cur}" in + tag:*) + COMPREPLY=( $(compgen -P "tag:" -W "`notmuch search --output=tags \*`" -- ${cur##tag:}) ) + ;; + to:*) + COMPREPLY=( $(compgen -P "to:" -W "`_notmuch_user_emails`" -- ${cur##to:}) ) + ;; + from:*) + COMPREPLY=( $(compgen -P "from:" -W "`_notmuch_user_emails`" -- ${cur##from:}) ) + ;; + *) + local search_terms="from: to: subject: attachment: tag: id: thread: folder: date:" + compopt -o nospace + COMPREPLY=( $(compgen -W "${search_terms}" -- ${cur}) ) + ;; + esac + # handle search prefixes and tags with colons + __ltrim_colon_completions "${cur}" +} - COMPREPLY=() +_notmuch_config() +{ + local cur prev words cword split + _init_completion || return + + case "${prev}" in + config) + COMPREPLY=( $(compgen -W "get set list" -- ${cur}) ) + ;; + get|set) + COMPREPLY=( $(compgen -W "`notmuch config list | sed 's/=.*\$//'`" -- ${cur}) ) + ;; + # these will also complete on config get, but we don't care + database.path) + _filedir + ;; + maildir.synchronize_flags) + COMPREPLY=( $(compgen -W "true false" -- ${cur}) ) + ;; + esac +} - case $COMP_CWORD in - 1) - COMPREPLY=( $(compgen -W "${commands}" -- ${current}) ) ;; - 2) - case $previous in - help) - COMPREPLY=( $(compgen -W "${help_options}" -- ${current}) ) ;; - search) - COMPREPLY=( $(compgen -W "${search_options}" -- ${current}) ) ;; - esac - ;; +_notmuch_count() +{ + local cur prev words cword split + _init_completion -s || return + + $split && + case "${prev}" in + --output) + COMPREPLY=( $( compgen -W "messages threads" -- "${cur}" ) ) + return + ;; + --exclude) + COMPREPLY=( $( compgen -W "true false" -- "${cur}" ) ) + return + ;; + esac + + ! $split && + case "${cur}" in + -*) + local options="--output= --exclude=" + compopt -o nospace + COMPREPLY=( $(compgen -W "$options" -- ${cur}) ) + ;; + *) + _notmuch_search_terms + ;; esac } -complete -o default -o bashdefault -F _notmuch notmuch +_notmuch_dump() +{ + local cur prev words cword split + _init_completion -s || return + + $split && + case "${prev}" in + --format) + COMPREPLY=( $( compgen -W "sup batch-tag" -- "${cur}" ) ) + return + ;; + --output) + _filedir + return + ;; + esac + + ! $split && + case "${cur}" in + -*) + local options="--format= --output=" + compopt -o nospace + COMPREPLY=( $(compgen -W "$options" -- ${cur}) ) + ;; + *) + _notmuch_search_terms + ;; + esac +} + +_notmuch_new() +{ + local cur prev words cword split + _init_completion || return + + case "${cur}" in + -*) + local options="--no-hooks" + COMPREPLY=( $(compgen -W "${options}" -- ${cur}) ) + ;; + esac +} + +_notmuch_reply() +{ + local cur prev words cword split + _init_completion -s || return + + $split && + case "${prev}" in + --format) + COMPREPLY=( $( compgen -W "default json sexp headers-only" -- "${cur}" ) ) + return + ;; + --reply-to) + COMPREPLY=( $( compgen -W "all sender" -- "${cur}" ) ) + return + ;; + esac + + ! $split && + case "${cur}" in + -*) + local options="--format= --format-version= --reply-to= --decrypt" + compopt -o nospace + COMPREPLY=( $(compgen -W "$options" -- ${cur}) ) + ;; + *) + _notmuch_search_terms + ;; + esac +} + +_notmuch_restore() +{ + local cur prev words cword split + _init_completion -s || return + + $split && + case "${prev}" in + --format) + COMPREPLY=( $( compgen -W "sup batch-tag auto" -- "${cur}" ) ) + return + ;; + --input) + _filedir + return + ;; + esac + + ! $split && + case "${cur}" in + -*) + local options="--format= --accumulate --input=" + compopt -o nospace + COMPREPLY=( $(compgen -W "$options" -- ${cur}) ) + ;; + esac +} + +_notmuch_search() +{ + local cur prev words cword split + _init_completion -s || return + + $split && + case "${prev}" in + --format) + COMPREPLY=( $( compgen -W "json sexp text text0" -- "${cur}" ) ) + return + ;; + --output) + COMPREPLY=( $( compgen -W "summary threads messages files tags" -- "${cur}" ) ) + return + ;; + --sort) + COMPREPLY=( $( compgen -W "newest-first oldest-first" -- "${cur}" ) ) + return + ;; + --exclude) + COMPREPLY=( $( compgen -W "true false flag" -- "${cur}" ) ) + return + ;; + esac + + ! $split && + case "${cur}" in + -*) + local options="--format= --output= --sort= --offset= --limit= --exclude=" + compopt -o nospace + COMPREPLY=( $(compgen -W "$options" -- ${cur}) ) + ;; + *) + _notmuch_search_terms + ;; + esac +} + +_notmuch_show() +{ + local cur prev words cword split + _init_completion -s || return + + $split && + case "${prev}" in + --entire-thread) + COMPREPLY=( $( compgen -W "true false" -- "${cur}" ) ) + return + ;; + --format) + COMPREPLY=( $( compgen -W "text json sexp mbox raw" -- "${cur}" ) ) + return + ;; + --exclude|--body) + COMPREPLY=( $( compgen -W "true false" -- "${cur}" ) ) + return + ;; + esac + + ! $split && + case "${cur}" in + -*) + local options="--entire-thread= --format= --exclude= --body= --format-version= --part= --verify --decrypt" + compopt -o nospace + COMPREPLY=( $(compgen -W "$options" -- ${cur}) ) + ;; + *) + _notmuch_search_terms + ;; + esac +} + +_notmuch_tag() +{ + local cur prev words cword split + # handle tags with colons and equal signs + _init_completion -n := || return + + case "${cur}" in + +*) + COMPREPLY=( $(compgen -P "+" -W "`notmuch search --output=tags \*`" -- ${cur##+}) ) + ;; + -*) + COMPREPLY=( $(compgen -P "-" -W "`notmuch search --output=tags \*`" -- ${cur##-}) ) + ;; + *) + _notmuch_search_terms + return + ;; + esac + # handle tags with colons + __ltrim_colon_completions "${cur}" +} + +_notmuch() +{ + local _notmuch_commands="config count dump help new reply restore search setup show tag" + local arg cur prev words cword split + _init_completion || return + + COMPREPLY=() + + # subcommand + _get_first_arg + + # complete --help option like the subcommand + if [ -z "${arg}" -a "${prev}" = "--help" ]; then + arg="help" + fi + + if [ -z "${arg}" ]; then + # top level completion + local top_options="--help --version" + case "${cur}" in + -*) COMPREPLY=( $(compgen -W "${top_options}" -- ${cur}) ) ;; + *) COMPREPLY=( $(compgen -W "${_notmuch_commands}" -- ${cur}) ) ;; + esac + elif [ "${arg}" = "help" ]; then + # handle help command specially due to _notmuch_commands usage + local help_topics="$_notmuch_commands hooks search-terms" + COMPREPLY=( $(compgen -W "${help_topics}" -- ${cur}) ) + else + # complete using _notmuch_subcommand if one exist + local completion_func="_notmuch_${arg//-/_}" + declare -f $completion_func >/dev/null && $completion_func + fi +} && +complete -F _notmuch notmuch diff --git a/contrib/notmuch-mutt/notmuch-mutt b/contrib/notmuch-mutt/notmuch-mutt index d14709df..00c5ef82 100755 --- a/contrib/notmuch-mutt/notmuch-mutt +++ b/contrib/notmuch-mutt/notmuch-mutt @@ -121,7 +121,8 @@ sub prompt($$) { sub get_message_id() { my $mail = Mail::Internet->new(\*STDIN); - $mail->head->get("message-id") =~ /^<(.*)>$/; # get message-id + my $mid = $mail->head->get("message-id") or return undef; + $mid =~ /^<(.*)>$/; # get message-id value return $1; } @@ -142,6 +143,10 @@ sub thread_action($$@) { my ($results_dir, $remove_dups, @params) = @_; my $mid = get_message_id(); + if (! defined $mid) { + empty_maildir($results_dir); + die "notmuch-mutt: cannot find Message-Id, abort.\n"; + } my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid"); my $tid = `$search_cmd`; # get thread id chomp($tid); @@ -151,6 +156,7 @@ sub thread_action($$@) { sub tag_action(@) { my $mid = get_message_id(); + defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n"; system("notmuch tag " . shell_quote(join(' ', @_)) @@ -264,13 +270,23 @@ 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 -r --prompt search<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter>" \ + "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ + <shell-escape>notmuch-mutt -r --prompt search<enter>\ + <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\ + <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ "notmuch: search mail" + macro index <F9> \ - "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt -r thread<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" \ + "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ + <pipe-message>notmuch-mutt -r thread<enter>\ + <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\ + <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ "notmuch: reconstruct thread" + macro index <F6> \ - "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -- -inbox<enter>" \ + "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ + <pipe-message>notmuch-mutt tag -- -inbox<enter>\ + <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ "notmuch: remove message from inbox" The first macro (activated by <F8>) prompts the user for notmuch search terms diff --git a/contrib/notmuch-mutt/notmuch-mutt.rc b/contrib/notmuch-mutt/notmuch-mutt.rc index ddc4b480..6b299dc9 100644 --- a/contrib/notmuch-mutt/notmuch-mutt.rc +++ b/contrib/notmuch-mutt/notmuch-mutt.rc @@ -1,9 +1,19 @@ macro index <F8> \ - "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt -r --prompt search<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>" \ +"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ +<shell-escape>notmuch-mutt -r --prompt search<enter>\ +<change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\ +<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ "notmuch: search mail" + macro index <F9> \ - "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt -r thread<enter><change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter><enter-command>set wait_key<enter>" \ +"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ +<pipe-message>notmuch-mutt -r thread<enter>\ +<change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\ +<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ "notmuch: reconstruct thread" + macro index <F6> \ - "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -- -inbox<enter>" \ +"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ +<pipe-message>notmuch-mutt tag -- -inbox<enter>\ +<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ "notmuch: remove message from inbox" diff --git a/contrib/notmuch-pick/notmuch-pick.el b/contrib/notmuch-pick/notmuch-pick.el index d75a66ae..128fabf8 100644 --- a/contrib/notmuch-pick/notmuch-pick.el +++ b/contrib/notmuch-pick/notmuch-pick.el @@ -155,6 +155,8 @@ ;; The context of the search: i.e., useful but can be dropped. (defvar notmuch-pick-query-context nil) (make-variable-buffer-local 'notmuch-pick-query-context) +(defvar notmuch-pick-target-msg nil) +(make-variable-buffer-local 'notmuch-pick-target-msg) (defvar notmuch-pick-buffer-name nil) (make-variable-buffer-local 'notmuch-pick-buffer-name) ;; This variable is the window used for the message pane. It is set @@ -328,7 +330,9 @@ Does NOT change the database." (defun notmuch-pick-from-show-current-query () "Call notmuch pick with the current query" (interactive) - (notmuch-pick notmuch-show-thread-id notmuch-show-query-context)) + (notmuch-pick notmuch-show-thread-id + notmuch-show-query-context + (notmuch-show-get-message-id))) ;; This function should be in notmuch.el but be we trying to minimise ;; impact on the rest of the codebase. @@ -344,6 +348,7 @@ Does NOT change the database." (interactive) (notmuch-pick (notmuch-search-find-thread-id) notmuch-search-query-string + nil (notmuch-prettify-subject (notmuch-search-find-subject))) (notmuch-pick-show-match-message-with-wait)) @@ -506,9 +511,13 @@ will be reversed." (let ((inhibit-read-only t) (basic-query notmuch-pick-basic-query) (query-context notmuch-pick-query-context) + (target (notmuch-pick-get-message-id)) (buffer-name notmuch-pick-buffer-name)) (erase-buffer) - (notmuch-pick-worker basic-query query-context (get-buffer buffer-name)))) + (notmuch-pick-worker basic-query + query-context + target + (get-buffer buffer-name)))) (defmacro with-current-notmuch-pick-message (&rest body) "Evaluate body with current buffer set to the text of current message" @@ -638,6 +647,17 @@ unchanged ADDRESS if parsing fails." (notmuch-pick-set-message-properties msg) (insert "\n")) +(defun notmuch-pick-goto-and-insert-msg (msg) + "Insert msg at the end of the buffer. Move point to msg if it is the target" + (save-excursion + (goto-char (point-max)) + (notmuch-pick-insert-msg msg)) + (let ((msg-id (notmuch-id-to-query (plist-get msg :id)))) + (when (string= msg-id notmuch-pick-target-msg) + (setq notmuch-pick-target-msg "found") + (goto-char (point-max)) + (forward-line -1)))) + (defun notmuch-pick-insert-tree (tree depth tree-status first last) "Insert the message tree TREE at depth DEPTH in the current thread." (let ((msg (car tree)) @@ -659,7 +679,7 @@ unchanged ADDRESS if parsing fails." (push "├" tree-status))) (push (concat (if replies "┬" "─") "â–º") tree-status) - (notmuch-pick-insert-msg (plist-put msg :tree-status tree-status)) + (notmuch-pick-goto-and-insert-msg (plist-put msg :tree-status tree-status)) (pop tree-status) (pop tree-status) @@ -678,12 +698,10 @@ unchanged ADDRESS if parsing fails." do (notmuch-pick-insert-tree tree depth tree-status (eq count 1) (eq count n))))) (defun notmuch-pick-insert-forest-thread (forest-thread) - (save-excursion - (goto-char (point-max)) - (let (tree-status) - ;; Reset at the start of each main thread. - (setq notmuch-pick-previous-subject nil) - (notmuch-pick-insert-thread forest-thread 0 tree-status)))) + (let (tree-status) + ;; Reset at the start of each main thread. + (setq notmuch-pick-previous-subject nil) + (notmuch-pick-insert-thread forest-thread 0 tree-status))) (defun notmuch-pick-insert-forest (forest) (mapc 'notmuch-pick-insert-forest-thread forest)) @@ -759,12 +777,13 @@ Complete list of currently available key bindings: 'notmuch-pick-show-error results-buf))))) -(defun notmuch-pick-worker (basic-query &optional query-context buffer) +(defun notmuch-pick-worker (basic-query &optional query-context target buffer) (interactive) (notmuch-pick-mode) (setq notmuch-pick-basic-query basic-query) (setq notmuch-pick-query-context query-context) (setq notmuch-pick-buffer-name (buffer-name buffer)) + (setq notmuch-pick-target-msg target) (erase-buffer) (goto-char (point-min)) @@ -796,7 +815,7 @@ Complete list of currently available key bindings: (insert "End of search results.\n")))))) -(defun notmuch-pick (&optional query query-context buffer-name show-first-match) +(defun notmuch-pick (&optional query query-context target buffer-name show-first-match) "Run notmuch pick with the given `query' and display the results" (interactive "sNotmuch pick: ") (if (null query) @@ -810,7 +829,7 @@ Complete list of currently available key bindings: ;; Don't track undo information for this buffer (set 'buffer-undo-list t) - (notmuch-pick-worker query query-context buffer) + (notmuch-pick-worker query query-context target buffer) (setq truncate-lines t) (when show-first-match diff --git a/vim/Makefile b/contrib/notmuch-vim/Makefile index f17bebfe..f17bebfe 100644 --- a/vim/Makefile +++ b/contrib/notmuch-vim/Makefile diff --git a/vim/README b/contrib/notmuch-vim/README index 53f1c4e1..53f1c4e1 100644 --- a/vim/README +++ b/contrib/notmuch-vim/README diff --git a/vim/notmuch.yaml b/contrib/notmuch-vim/notmuch.yaml index 3d8422c8..3d8422c8 100644 --- a/vim/notmuch.yaml +++ b/contrib/notmuch-vim/notmuch.yaml diff --git a/vim/plugin/notmuch.vim b/contrib/notmuch-vim/plugin/notmuch.vim index 8f27fb92..8f27fb92 100644 --- a/vim/plugin/notmuch.vim +++ b/contrib/notmuch-vim/plugin/notmuch.vim diff --git a/vim/syntax/notmuch-compose.vim b/contrib/notmuch-vim/syntax/notmuch-compose.vim index 19adb756..19adb756 100644 --- a/vim/syntax/notmuch-compose.vim +++ b/contrib/notmuch-vim/syntax/notmuch-compose.vim diff --git a/vim/syntax/notmuch-folders.vim b/contrib/notmuch-vim/syntax/notmuch-folders.vim index 9477f86f..9477f86f 100644 --- a/vim/syntax/notmuch-folders.vim +++ b/contrib/notmuch-vim/syntax/notmuch-folders.vim diff --git a/vim/syntax/notmuch-git-diff.vim b/contrib/notmuch-vim/syntax/notmuch-git-diff.vim index 6f15fdc7..6f15fdc7 100644 --- a/vim/syntax/notmuch-git-diff.vim +++ b/contrib/notmuch-vim/syntax/notmuch-git-diff.vim diff --git a/vim/syntax/notmuch-search.vim b/contrib/notmuch-vim/syntax/notmuch-search.vim index f458d778..f458d778 100644 --- a/vim/syntax/notmuch-search.vim +++ b/contrib/notmuch-vim/syntax/notmuch-search.vim diff --git a/vim/syntax/notmuch-show.vim b/contrib/notmuch-vim/syntax/notmuch-show.vim index c3a98b77..c3a98b77 100644 --- a/vim/syntax/notmuch-show.vim +++ b/contrib/notmuch-vim/syntax/notmuch-show.vim @@ -20,6 +20,48 @@ #include "notmuch-client.h" +#ifdef GMIME_ATLEAST_26 + +/* Create a GPG context (GMime 2.6) */ +static notmuch_crypto_context_t * +create_gpg_context (void) +{ + notmuch_crypto_context_t *gpgctx; + + /* TODO: GMimePasswordRequestFunc */ + gpgctx = g_mime_gpg_context_new (NULL, "gpg"); + if (! gpgctx) + return NULL; + + g_mime_gpg_context_set_use_agent ((GMimeGpgContext *) gpgctx, TRUE); + g_mime_gpg_context_set_always_trust ((GMimeGpgContext *) gpgctx, FALSE); + + return gpgctx; +} + +#else /* GMIME_ATLEAST_26 */ + +/* Create a GPG context (GMime 2.4) */ +static notmuch_crypto_context_t * +create_gpg_context (void) +{ + GMimeSession *session; + notmuch_crypto_context_t *gpgctx; + + session = g_object_new (g_mime_session_get_type (), NULL); + gpgctx = g_mime_gpg_context_new (session, "gpg"); + g_object_unref (session); + + if (! gpgctx) + return NULL; + + g_mime_gpg_context_set_always_trust ((GMimeGpgContext *) gpgctx, FALSE); + + return gpgctx; +} + +#endif /* GMIME_ATLEAST_26 */ + /* for the specified protocol return the context pointer (initializing * if needed) */ notmuch_crypto_context_t * @@ -33,25 +75,14 @@ notmuch_crypto_get_context (notmuch_crypto_t *crypto, const char *protocol) * parameter names as defined in this document are * case-insensitive." Thus, we use strcasecmp for the protocol. */ - if ((strcasecmp (protocol, "application/pgp-signature") == 0) - || (strcasecmp (protocol, "application/pgp-encrypted") == 0)) { - if (!crypto->gpgctx) { -#ifdef GMIME_ATLEAST_26 - /* TODO: GMimePasswordRequestFunc */ - crypto->gpgctx = g_mime_gpg_context_new (NULL, "gpg"); -#else - GMimeSession* session = g_object_new (g_mime_session_get_type(), NULL); - crypto->gpgctx = g_mime_gpg_context_new (session, "gpg"); - g_object_unref (session); -#endif - if (crypto->gpgctx) { - g_mime_gpg_context_set_always_trust ((GMimeGpgContext*) crypto->gpgctx, FALSE); - } else { + if (strcasecmp (protocol, "application/pgp-signature") == 0 || + strcasecmp (protocol, "application/pgp-encrypted") == 0) { + if (! crypto->gpgctx) { + crypto->gpgctx = create_gpg_context (); + if (! crypto->gpgctx) fprintf (stderr, "Failed to construct gpg context.\n"); - } } cryptoctx = crypto->gpgctx; - } else { fprintf (stderr, "Unknown or unsupported cryptographic protocol.\n"); } diff --git a/debian/NEWS.Debian b/debian/NEWS.Debian index 907d7907..1dd9e0d0 100644 --- a/debian/NEWS.Debian +++ b/debian/NEWS.Debian @@ -1,3 +1,10 @@ +notmuch (0.16-1) unstable; urgency=low + + * The vim interface is no longer provided as a Debian package, due + to upstream deprecation. + + -- David Bremner <bremner@debian.org> Sat, 16 Feb 2013 08:12:02 -0400 + notmuch (0.14-1) unstable; urgency=low There is an incompatible change in option syntax for dump and restore diff --git a/debian/control b/debian/control index e571624a..08071bee 100644 --- a/debian/control +++ b/debian/control @@ -15,6 +15,7 @@ Build-Depends: libz-dev, python-all (>= 2.6.6-3~), python3-all (>= 3.1.2-7~), + ruby, ruby-dev, emacs23-nox | emacs23 (>=23~) | emacs23-lucid (>=23~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~), gdb, @@ -89,36 +90,34 @@ Description: Python 3 interface to the notmuch mail search and index library This package provides a Python 3 interface to the notmuch functionality, directly interfacing with a shared notmuch library. -Package: notmuch-emacs -Architecture: all -Section: mail -Breaks: notmuch (<<0.6~254~) -Replaces: notmuch (<<0.6~254~) -Depends: ${misc:Depends}, notmuch (>= ${source:Version}), - emacs23 (>= 23~) | emacs23-nox (>=23~) | emacs23-lucid (>=23~) | - emacs24 (>= 24~) | emacs24-nox (>=24~) | emacs24-lucid (>=24~) -Description: thread-based email index, search and tagging (emacs interface) +Package: notmuch-ruby +Architecture: any +Section: ruby +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Ruby interface to the notmuch mail search and index library Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses the Xapian library to provide fast, full-text search with a very convenient search syntax. . - This package provides an emacs based mail user agent based on - notmuch. + This package provides a Ruby interface to the notmuch + functionality, directly interfacing with a shared notmuch library. -Package: notmuch-vim +Package: notmuch-emacs Architecture: all Section: mail Breaks: notmuch (<<0.6~254~) Replaces: notmuch (<<0.6~254~) -Depends: ${misc:Depends}, notmuch, vim-addon-manager -Description: thread-based email index, search and tagging (vim interface) +Depends: ${misc:Depends}, notmuch (>= ${source:Version}), + emacs23 (>= 23~) | emacs23-nox (>=23~) | emacs23-lucid (>=23~) | + emacs24 (>= 24~) | emacs24-nox (>=24~) | emacs24-lucid (>=24~) +Description: thread-based email index, search and tagging (emacs interface) Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses the Xapian library to provide fast, full-text search with a very convenient search syntax. . - This package provides a vim based mail user agent based on + This package provides an emacs based mail user agent based on notmuch. Package: notmuch-mutt diff --git a/debian/notmuch-ruby.install b/debian/notmuch-ruby.install new file mode 100644 index 00000000..98e7050b --- /dev/null +++ b/debian/notmuch-ruby.install @@ -0,0 +1 @@ +usr/lib/ruby/vendor_ruby/*/*/notmuch.so diff --git a/debian/rules b/debian/rules index c4e3930d..71a56028 100755 --- a/debian/rules +++ b/debian/rules @@ -12,15 +12,18 @@ override_dh_auto_build: dh_auto_build dh_auto_build --sourcedirectory bindings/python cd bindings/python && $(python3_all) setup.py build + cd bindings/ruby && ruby extconf.rb --vendor && make $(MAKE) -C contrib/notmuch-mutt override_dh_auto_clean: dh_auto_clean dh_auto_clean --sourcedirectory bindings/python cd bindings/python && $(python3_all) setup.py clean -a + dh_auto_clean --sourcedirectory bindings/ruby $(MAKE) -C contrib/notmuch-mutt clean override_dh_auto_install: dh_auto_install dh_auto_install --sourcedirectory bindings/python cd bindings/python && $(python3_all) setup.py install --install-layout=deb --root=$(CURDIR)/debian/tmp + dh_auto_install --sourcedirectory bindings/ruby diff --git a/devel/STYLE b/devel/STYLE index 0792ba12..92de42cc 100644 --- a/devel/STYLE +++ b/devel/STYLE @@ -45,8 +45,9 @@ function (param_type param, param_type param) - likewise, there is a space following keywords such as if and while - every binary operator should have space on either side. -* No trailing whitespace. Please enable the standard pre-commit hook - in git (or an equivalent hook). +* No trailing whitespace. Please enable the standard pre-commit hook in git + (or an equivalent hook). The standard pre-commit hook is enabled by simply + renaming file '.git/hooks/pre-commit.sample' to '.git/hooks/pre-commit' . * The name in a function prototype should start at the beginning of a line. @@ -57,17 +57,6 @@ Automatically open a message when navigating to it with N or P. Change 'a' command in thread-view mode to only archive open messages. -Add a binding to open all closed messages. - -Change the 'a'rchive command in the thread view to only archive open -messages. - -Completion ----------- -Fix bash completion to complete multiple search options (both --first -and *then* --max-threads), and also complete value for --sort= -(oldest-first or newest-first). - notmuch command-line tool ------------------------- Add support to "notmuch search" and "notmuch show" to allow for @@ -75,16 +64,6 @@ listing of duplicate messages, (distinct filenames with the same Message-ID). I'm not sure what the option should be named. Perhaps --with-duplicates ? -Add a -0 option to "notmuch search" so that one can safely deal with -any filename with: - - notmuch search --output=files -0 <terms> | xargs -0 <command> - -"notmuch setup" should use realpath() before replacing the -configuration file. The ensures that the final target file of any -intermediate symbolic links is what is actually replaced, (rather than -any symbolic link). - Replace "notmuch reply" with "notmuch compose --reply <search-terms>". This would enable a plain "notmuch compose" to be used to construct an initial message, (which would then have the properly configured name @@ -112,19 +91,9 @@ Fix "notmuch restore" to operate in a single pass much like "notmuch dump" does, rather than doing N searches into the database, each matching 1/N messages. -Add a "-f <filename>" option to select an alternate configuration -file. - Allow configuration for filename patterns that should be ignored when indexing. -Replace the "notmuch part --part=id" command with "notmuch show ---part=id", (David Edmondson wants to rewrite some of "notmuch show" to -provide more MIME-structure information in its output first). - -Replace the "notmuch search-tags" command with "notmuch search ---output=tags". - Fix to avoid this ugly message: (process:17197): gmime-CRITICAL **: g_mime_message_get_mime_part: assertion `GMIME_IS_MESSAGE (message)' failed @@ -163,12 +132,13 @@ vs. tag-when-all-files-flagged (* above)). Add an interface to accept a "key" and a byte stream, rather than a filename. -Provide a sane syntax for date ranges. First, we don't want to require -both endpoints to be specified. For example it would be nice to be -able to say things like "since:2009-01-1" or "until:2009-01-1" and -have the other endpoint be implicit. Second we'd like to support -relative specifications of time such as "since:'2 months ago'". To do -any of this we're probably going to need to break down an write our +Improve syntax for date ranges queries. date:expr should be +interpreted as date:expr..expr so that, for example, "date:2013-01-22" +would cover the whole of the specified day (currently that's not even +recognized as a date range expression). It might be nice to be able to +use things like "since:2013-01-22" and "until:2013-01-22" as synonyms +to "date:2013-01-22.." and "date:..2013-01-22", respectively. To do +any of this we're probably going to need to break down and write our own parser for the query string rather than using Xapian's QueryParser class. diff --git a/devel/man-to-mdwn.pl b/devel/man-to-mdwn.pl new file mode 100755 index 00000000..4b59bd66 --- /dev/null +++ b/devel/man-to-mdwn.pl @@ -0,0 +1,197 @@ +#!/usr/bin/perl +# +# Author: Tomi Ollila +# License: same as notmuch +# +# This program is used to generate mdwn-formatted notmuch manual pages +# for notmuch wiki. Example run: +# +# $ ./devel/man-to-mdwn.pl man ../notmuch-wiki +# +# In case taken into more generic use, modify these comments and examples. + +use 5.8.1; +use strict; +use warnings; + +unless (@ARGV == 2) { + warn "\n$0 <source-directory> <destination-directory>\n\n"; + # Remove/edit this comment if this script is taken into generic use. + warn "Example: ./devel/man-to-mdwn.pl man ../notmuch-wiki\n\n"; + exit 1; +} + +die "'$ARGV[0]': no such source directory\n" unless -d $ARGV[0]; +die "'$ARGV[1]': no such destination directory\n" unless -d $ARGV[1]; + +#die "'manpages' exists\n" if -e 'manpages'; +#die "'manpages.mdwn' exists\n" if -e 'manpages.mdwn'; + +die "Expecting '$ARGV[1]/manpages' to exist.\n" . + "Please create it first or adjust <destination-directory>.\n" + unless -d $ARGV[1] . '/manpages'; + +my $ev = 0; +my %fhash; + +open P, '-|', 'find', $ARGV[0], qw/-name *.[0-9] -print/; +while (<P>) +{ + chomp; + next unless -f $_; # follows symlink. + $ev = 1, warn "'$_': no such file\n" unless -f $_; + my ($in, $on) = ($_, $_); + $on =~ s|.*/||; $on =~ tr/./-/; + my $f = $fhash{$on}; + $ev = 1, warn "'$in' collides with '$f' ($on.mdwn)\n" if defined $f; + $fhash{$on} = $in; +} +close P; + +#undef $ENV{'GROFF_NO_SGR'}; +#delete $ENV{'GROFF_NO_SGR'}; +$ENV{'GROFF_NO_SGR'} = '1'; +$ENV{'TERM'} = 'vt100'; # does this matter ? + +my %htmlqh = qw/& & < < > > ' ' " "/; +# do html quotation to $_[0] (which is an alias to the given arg) +sub htmlquote($) +{ + $_[0] =~ s/([&<>'"])/$htmlqh{$1}/ge; +} + +sub maymakelink($); +sub mayconvert($$); + +#warn keys %fhash, "\n"; + +while (my ($k, $v) = each %fhash) +{ + #next if -l $v; # skip symlinks here. -- not... references there may be. + + my @lines; + #open I, '-|', qw/groff -man -T utf8/, $v; + open I, '-|', qw/groff -man -T latin1/, $v; # this and GROFF_NO_SGR='1' + + my ($emptyline, $pre, $hl) = (0, 0, 'h1'); + while (<I>) { + if (/^\s*$/) { + $emptyline = 1; + next; + } + s/(?<=\S)\s{8,}.*//; # $hl = 'h1' if s/(?<=\S)\s{8,}.*//; + htmlquote $_; + s/[_&]\010&/&/g; + s/((?:_\010[^_])+)/<u>$1<\/u>/g; + s/_\010(.)/$1/g; + s/((?:.\010.)+)/<b>$1<\/b>/g; + s/.\010(.)/$1/g; + + if (/^\S/) { + $pre = 0, push @lines, "</pre>\n" if $pre; + s/<\/?b>//g; + chomp; + $_ = "\n<$hl>$_</$hl>\n"; + $hl = 'h2'; + $emptyline = 0; + } + elsif (/^\s\s\s\S/) { + $pre = 0, push @lines, "</pre>\n" if $pre; + s/(?:^\s+)?<\/?b>//g; + chomp; + $_ = "\n<h3> $_</h3>\n"; + $emptyline = 0; + } + else { + $pre = 1, push @lines, "<pre>\n" unless $pre; + $emptyline = 0, push @lines, "\n" if $emptyline; + } + push @lines, $_; + } + $lines[0] =~ s/^\n//; + $k = "$ARGV[1]/manpages/$k.mdwn"; + open O, '>', $k or die; + print STDOUT 'Writing ', "'$k'\n"; + select O; + my $pe = ''; + foreach (@lines) { + if ($pe) { + if (s/^(\s+)<b>([^<]+)<\/b>\((\d+)\)//) { + my $link = maymakelink "$pe-$2-$3"; + $link = maymakelink "$pe$2-$3" unless $link; + if ($link) { + print "<a href='$link'>$pe-</a>\n"; + print "$1<a href='$link'>$2</a>($3)"; + } + else { + print "<b>$pe-</b>\n"; + print "$1<b>$2</b>($3)"; + } + } else { + print "<b>$pe-</b>\n"; + } + $pe = ''; + } + s/<b>([^<]+)<\/b>\((\d+)\)/mayconvert($1, $2)/ge; + $pe = $1 if s/<b>([^<]+)-<\/b>\s*$//; + print $_; + } +} + +sub maymakelink($) +{ +# warn "$_[0]\n"; + return "../$_[0]/" if exists $fhash{$_[0]}; + return ''; +} + +sub mayconvert($$) +{ + my $f = "$_[0]-$_[1]"; +# warn "$f\n"; + return "<a href='../$f/'>$_[0]</a>($_[1])" if exists $fhash{$f}; + return "<b>$_[0]</b>($_[1])"; +} + +# Finally, make manpages.mdwn + +open O, '>', $ARGV[1] . '/manpages.mdwn' or die $!; +print STDOUT "Writing '$ARGV[1]/manpages.mdwn'\n"; +select O; +print "Manual page index\n"; +print "=================\n\n"; + +sub srt { my ($x, $y) = ($a, $b); $x =~ tr/./-/; $y =~ tr/./-/; $x cmp $y; } + +foreach (sort srt values %fhash) +{ + my $in = $_; + open I, '<', $in or die $!; + my $s; + while (<I>) { + if (/^\s*[.]TH\s+\S+\s+(\S+)/) { + $s = $1; + last; + } + } + while (<I>) { + last if /^\s*[.]SH NAME/ + } + my $line = ''; + while (<I>) { + tr/\\//d; + if (/\s*(\S+)\s+(.*)/) { + my $e = $2; + # Ignoring the NAME in file, get from file name instead. + #my $on = (-l $in)? readlink $in: $in; + my $on = $in; + $on =~ tr/./-/; $on =~ s|.*/||; + my $n = $in; $n =~ s|.*/||; $n =~ tr/./-/; $n =~ s/-[^-]+$//; + $line = "<a href='$on/'>$n</a>($s) $e\n"; + last; + } + } + die "No NAME in '$in'\n" unless $line; + print "* $line"; + #warn $line; +} diff --git a/devel/news2wiki.pl b/devel/news2wiki.pl new file mode 100755 index 00000000..8066ba7f --- /dev/null +++ b/devel/news2wiki.pl @@ -0,0 +1,102 @@ +#!/usr/bin/perl +# +# Author: Tomi Ollila +# License: same as notmuch + +# This program is used to split NEWS file to separate (mdwn) files +# for notmuch wiki. Example run: +# +# $ ./devel/news2wiki.pl NEWS ../notmuch-wiki/news +# +# In case taken into more generic use, modify these comments and examples. + +use strict; +use warnings; + +unless (@ARGV == 2) { + warn "\n$0 <source-file> <destination-directory>\n\n"; + warn "Example: ./devel/news2wiki.pl NEWS ../notmuch-wiki/news\n\n"; + exit 1; +} + +die "'$ARGV[0]': no such file\n" unless -f $ARGV[0]; +die "'$ARGV[1]': no such directory\n" unless -d $ARGV[1]; + +open I, '<', $ARGV[0] or die "Cannot open '$ARGV[0]': $!\n"; + +open O, '>', '/dev/null' or die $!; +my @emptylines = (); +my $cln; +print "\nWriting to $ARGV[1]:\n"; +while (<I>) +{ + warn "$ARGV[0]:$.: tab(s) in line!\n" if /\t/; + warn "$ARGV[0]:$.: trailing whitespace\n" if /\s\s$/; + # The date part in regex recognizes wip version dates like: (201x-xx-xx). + if (/^Notmuch\s+(\S+)\s+\((\w\w\w\w-\w\w-\w\w)\)\s*$/) { + # open O... autocloses previously opened file. + open O, '>', "$ARGV[1]/release-$1.mdwn" or die $!; + print "+ release-$1.mdwn...\n"; + print O "[[!meta date=\"$2\"]]\n\n"; + @emptylines = (); + } + + last if /^<!--\s*$/; # Local variables block at the end (as of now). + + # Buffer "trailing" empty lines -- dropped at end of file. + push(@emptylines, $_), next if s/^\s*$/\n/; + if (@emptylines) { + print O @emptylines; + @emptylines = (); + } + + # Convert '*' to '`*`' and "*" to "`*`" so that * is not considered + # as starting emphasis character there. We're a bit opportunistic + # there -- some single * does not cause problems and, on the other + # hand, this would not regognize already 'secured' *:s. + s/'[*]'/'`*`'/g; s/"[*]"/"`*`"/g; + + # Convert nonindented lines that aren't already headers or + # don't contain periods (.) or '!'s to level 4 header. + if ( /^[^\s-]/ ) { + my $tbc = ! /[.!]\s/; + chomp; + my @l = $_; + $cln = $.; + while (<I>) { + last if /^\s*$/; + #$cln = 0 if /^---/ or /^===/; # used for debugging. + $tbc = 0 if /[.!]\s/ or /^---/ or /^===/; + chomp; s/^\s+//; + push @l, $_; + } + if ($tbc) { + print O "### ", (join ' ', @l), "\n"; + } + else { + #print "$ARGV[0]:$cln: skip level 4 header conversion\n" if $cln; + print O (join "\n", @l), "\n"; + } + @emptylines = ( "\n" ); + next; + } + + # Markdown doc specifies that list item may have paragraphs if those + # are indented by 4 spaces (or a tab) from current list item marker + # indentation (paragraph meaning there is empty line in between). + # If there is empty line and next line is not indented 4 chars then + # that should end the above list. This doesn't happen in all markdown + # implementations. + # In our NEWS case this problem exists in release 0.6 documentation. + # It can be avoided by removing 2 leading spaces in lines that are not + # list items and requiring all that indents are 0, 2, and 4+ (to make + # regexp below work). + # Nested lists are supported but one needs to be more careful with + # markup there (as the hack below works only on first level). + + s/^[ ][ ]// unless /^[ ][ ](?:[\s*+-]|\d+\.)\s/; + + print O $_; +} +print "\ndone.\n"; +close O; diff --git a/contrib/nmbug/nmbug b/devel/nmbug/nmbug index f003ef9e..90d98b63 100755 --- a/contrib/nmbug/nmbug +++ b/devel/nmbug/nmbug @@ -13,10 +13,7 @@ my $NMBGIT = $ENV{NMBGIT} || $ENV{HOME}.'/.nmbug'; $NMBGIT .= '/.git' if (-d $NMBGIT.'/.git'); -my $TAGPREFIX = $ENV{NMBPREFIX} || 'notmuch::'; - -# magic hash for git -my $EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; +my $TAGPREFIX = defined($ENV{NMBPREFIX}) ? $ENV{NMBPREFIX} : 'notmuch::'; # for encoding @@ -39,12 +36,20 @@ my %command = ( status => \&do_status, ); +# Convert prefix into form suitable for literal matching against +# notmuch dump --format=batch-tag output. +my $ENCPREFIX = encode_for_fs ($TAGPREFIX); +$ENCPREFIX =~ s/:/%3a/g; + my $subcommand = shift || usage (); if (!exists $command{$subcommand}) { usage (); } +# magic hash for git +my $EMPTYBLOB = git (qw{hash-object -t blob /dev/null}); + &{$command{$subcommand}}(@ARGV); sub git_pipe { @@ -203,9 +208,9 @@ sub index_tags { my $index = $NMBGIT.'/nmbug.index'; - my $query = join ' ', map ("tag:$_", get_tags ($TAGPREFIX)); + my $query = join ' ', map ("tag:\"$_\"", get_tags ($TAGPREFIX)); - my $fh = spawn ('-|', qw/notmuch dump --/, $query) + my $fh = spawn ('-|', qw/notmuch dump --format=batch-tag --/, $query) or die "notmuch dump: $!"; git ('read-tree', '--empty'); @@ -214,22 +219,30 @@ sub index_tags { or die 'git update-index'; while (<$fh>) { - m/ ( [^ ]* ) \s+ \( ([^\)]* ) \) /x || die 'syntax error in dump'; - my ($id,$rest) = ($1,$2); - #strip prefixes before writing - my @tags = grep { s/^$TAGPREFIX//; } split (' ', $rest); + chomp(); + my ($rest,$id) = split(/ -- id:/); + + if ($id =~ s/^"(.*)"\s*$/$1/) { + # xapian quoted string, dequote. + $id =~ s/""/"/g; + } + + #strip prefixes from tags before writing + my @tags = grep { s/^[+]$ENCPREFIX//; } split (' ', $rest); index_tags_for_msg ($git,$id, 'A', @tags); } unless (close $git) { die "'git update-index --index-info' exited with nonzero value\n"; } unless (close $fh) { - die "'notmuch dump -- $query' exited with nonzero value\n"; + die "'notmuch dump --format=batch-tag -- $query' exited with nonzero value\n"; } return $index; } +# update the git index to either create or delete an empty file. +# Neither argument should be encoded/escaped. sub index_tags_for_msg { my $fh = shift; my $msgid = shift; @@ -254,6 +267,20 @@ sub do_checkout { do_sync (action => 'checkout'); } +sub quote_for_xapian { + my $str = shift; + $str =~ s/"/""/g; + return '"' . $str . '"'; +} + +sub pair_to_batch_line { + my ($action, $pair) = @_; + + # the tag should already be suitably encoded + + return $action . $ENCPREFIX . $pair->{tag} . + ' -- id:' . quote_for_xapian ($pair->{id})."\n"; +} sub do_sync { @@ -270,17 +297,20 @@ sub do_sync { $D_action = '-'; } - foreach my $pair (@{$status->{added}}) { + my $notmuch = spawn ({}, '|-', qw/notmuch tag --batch/) + or die 'notmuch tag --batch'; - notmuch ('tag', $A_action.$TAGPREFIX.$pair->{tag}, - 'id:'.$pair->{id}); + foreach my $pair (@{$status->{added}}) { + print $notmuch pair_to_batch_line ($A_action, $pair); } foreach my $pair (@{$status->{deleted}}) { - notmuch ('tag', $D_action.$TAGPREFIX.$pair->{tag}, - 'id:'.$pair->{id}); + print $notmuch pair_to_batch_line ($D_action, $pair); } + unless (close $notmuch) { + die "'notmuch tag --batch' exited with nonzero value\n"; + } } @@ -331,7 +361,7 @@ sub do_log { sub do_push { my $remote = shift || 'origin'; - git ('push', $remote); + git ('push', $remote, 'master'); } diff --git a/contrib/nmbug/nmbug-status b/devel/nmbug/nmbug-status index d08ca08d..934c895f 100755 --- a/contrib/nmbug/nmbug-status +++ b/devel/nmbug/nmbug-status @@ -7,12 +7,12 @@ # - argparse; either python 2.7, or install separately import datetime -import notmuch import rfc822 import urllib import json import argparse import os +import sys import subprocess # parse command line arguments @@ -20,9 +20,10 @@ import subprocess parser = argparse.ArgumentParser() parser.add_argument('--text', help='output plain text format', action='store_true') - parser.add_argument('--config', help='load config from given file') - +parser.add_argument('--list-views', help='list views', + action='store_true') +parser.add_argument('--get-query', help='get query for view') args = parser.parse_args() @@ -46,6 +47,19 @@ else: config = json.load(fp) +if args.list_views: + for view in config['views']: + print view['title'] + sys.exit(0) +elif args.get_query != None: + for view in config['views']: + if args.get_query == view['title']: + print ' and '.join(view['query']) + sys.exit(0) +else: + # only import notmuch if needed + import notmuch + if args.text: output_format = 'text' else: diff --git a/contrib/nmbug/status-config.json b/devel/nmbug/status-config.json index 6b4934fa..6b4934fa 100644 --- a/contrib/nmbug/status-config.json +++ b/devel/nmbug/status-config.json diff --git a/devel/printmimestructure b/devel/printmimestructure new file mode 100755 index 00000000..34d12930 --- /dev/null +++ b/devel/printmimestructure @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net> +# License: GPLv3+ + +# This script reads a MIME message from stdin and produces a treelike +# representation on it stdout. + +# Example: +# +# 0 dkg@alice:~$ printmimestructure < 'Maildir/cur/1269025522.M338697P12023.monkey,S=6459,W=6963:2,Sa' +# └┬╴multipart/signed 6546 bytes +# ├─╴text/plain inline 895 bytes +# └─╴application/pgp-signature inline [signature.asc] 836 bytes +# 0 dkg@alice:~$ + + +# If you want to number the parts, i suggest piping the output through +# something like "cat -n" + +import email +import sys + +def test(z, prefix=''): + fname = '' if z.get_filename() is None else ' [' + z.get_filename() + ']' + cset = '' if z.get_charset() is None else ' (' + z.get_charset() + ')' + disp = z.get_params(None, header='Content-Disposition') + if (disp is None): + disposition = '' + else: + disposition = '' + for d in disp: + if d[0] in [ 'attachment', 'inline' ]: + disposition = ' ' + d[0] + if (z.is_multipart()): + print prefix + '┬╴' + z.get_content_type() + cset + disposition + fname, z.as_string().__len__().__str__() + ' bytes' + if prefix.endswith('â””'): + prefix = prefix.rpartition('â””')[0] + ' ' + if prefix.endswith('├'): + prefix = prefix.rpartition('├')[0] + '│' + parts = z.get_payload() + i = 0 + while (i < parts.__len__()-1): + test(parts[i], prefix + '├') + i += 1 + test(parts[i], prefix + 'â””') + # FIXME: show epilogue? + else: + print prefix + '─╴'+ z.get_content_type() + cset + disposition + fname, z.get_payload().__len__().__str__(), 'bytes' + +test(email.message_from_file(sys.stdin), 'â””') diff --git a/devel/release-checks.sh b/devel/release-checks.sh index e1d19f20..4eff1a7f 100755 --- a/devel/release-checks.sh +++ b/devel/release-checks.sh @@ -53,12 +53,13 @@ fi < ./version readonly VERSION +# In the rest of this file, tests collect list of errors to be fixed + verfail () { echo No. - echo "$@" - echo "Please follow the instructions in RELEASING to choose a version" - exit 1 + append_emsg "$@" + append_emsg " Please follow the instructions in RELEASING to choose a version" } echo -n "Checking that '$VERSION' is good with digits and periods... " @@ -73,8 +74,6 @@ case $VERSION in esac -# In the rest of this file, tests collect list of errors to be fixed - echo -n "Checking that this is Debian package for notmuch... " read deb_notmuch deb_version rest < debian/changelog if [ "$deb_notmuch" = 'notmuch' ] @@ -105,6 +104,20 @@ else append_emsg "Version '$py_version' is not '$VERSION' in $PV_FILE" fi +echo -n "Checking that NEWS header is tidy... " +if [ "`exec sed 's/./=/g; 1q' NEWS`" = "`exec sed '1d; 2q' NEWS`" ] +then + echo Yes. +else + echo No. + if [ "`exec sed '1d; s/=//g; 2q' NEWS`" != '' ] + then + append_emsg "Line 2 in NEWS file is not all '=':s" + else + append_emsg "Line 2 in NEWS file does not have the same length as line 1" + fi +fi + echo -n "Checking that this is Notmuch NEWS... " read news_notmuch news_version news_date < NEWS if [ "$news_notmuch" = "Notmuch" ] diff --git a/emacs/Makefile.local b/emacs/Makefile.local index fb82247f..456700ac 100644 --- a/emacs/Makefile.local +++ b/emacs/Makefile.local @@ -22,6 +22,18 @@ emacs_images := \ emacs_bytecode = $(emacs_sources:.el=.elc) +# Because of defmacro's and defsubst's, we have to account for load +# dependencies between Elisp files when byte compiling. Otherwise, +# the byte compiler may load an old .elc file when processing a +# "require" or we may fail to rebuild a .elc that depended on a macro +# from an updated file. +$(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources) + $(call quiet,EMACS) --directory emacs -batch -l make-deps.el \ + -f batch-make-deps $(emacs_sources) > $@.tmp && \ + (cmp -s $@.tmp $@ || mv $@.tmp $@) +-include $(dir)/.eldeps +CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp + %.elc: %.el $(global_deps) $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $< diff --git a/emacs/make-deps.el b/emacs/make-deps.el new file mode 100644 index 00000000..a1cd731f --- /dev/null +++ b/emacs/make-deps.el @@ -0,0 +1,66 @@ +;; make-deps.el --- compute make dependencies for Elisp sources +;; +;; Copyright © Austin Clements +;; +;; 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: Austin Clements <aclements@csail.mit.edu> + +(defun batch-make-deps () + "Invoke `make-deps' for each file on the command line." + + (setq debug-on-error t) + (dolist (file command-line-args-left) + (let ((default-directory command-line-default-directory)) + (find-file-literally file)) + (make-deps command-line-default-directory)) + (kill-emacs)) + +(defun make-deps (&optional dir) + "Print make dependencies for the current buffer. + +This prints make dependencies to `standard-output' based on the +top-level `require' expressions in the current buffer. Paths in +rules will be given relative to DIR, or `default-directory'." + + (setq dir (or dir default-directory)) + (save-excursion + (goto-char (point-min)) + (condition-case nil + (while t + (let ((form (read (current-buffer)))) + ;; Is it a (require 'x) form? + (when (and (listp form) (= (length form) 2) + (eq (car form) 'require) + (listp (cadr form)) (= (length (cadr form)) 2) + (eq (car (cadr form)) 'quote) + (symbolp (cadr (cadr form)))) + ;; Find the required library + (let* ((name (cadr (cadr form))) + (fname (locate-library (symbol-name name)))) + ;; Is this file and the library in the same directory? + ;; If not, assume it's a system library and don't + ;; bother depending on it. + (when (and fname + (string= (file-name-directory (buffer-file-name)) + (file-name-directory fname))) + ;; Print the dependency + (princ (format "%s.elc: %s.elc\n" + (file-name-sans-extension + (file-relative-name (buffer-file-name) dir)) + (file-name-sans-extension + (file-relative-name fname dir))))))))) + (end-of-file nil)))) diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el index 2bf762ba..fa65cd52 100644 --- a/emacs/notmuch-address.el +++ b/emacs/notmuch-address.el @@ -31,6 +31,23 @@ line." :group 'notmuch-send :group 'notmuch-external) +(defcustom notmuch-address-selection-function 'notmuch-address-selection-function + "The function to select address from given list. The function is +called with PROMPT, COLLECTION, and INITIAL-INPUT as arguments +(subset of what `completing-read' can be called with). +While executed the value of `completion-ignore-case' is t. +See documentation of function `notmuch-address-selection-function' +to know how address selection is made by default." + :type 'function + :group 'notmuch-send + :group 'notmuch-external) + +(defun notmuch-address-selection-function (prompt collection initial-input) + "Call (`completing-read' + PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)" + (completing-read + prompt collection nil nil initial-input 'notmuch-address-history)) + (defvar notmuch-address-message-alist-member '("^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):" . notmuch-address-expand-name)) @@ -61,9 +78,9 @@ line." ((eq num-options 1) (car options)) (t - (completing-read (format "Address (%s matches): " num-options) - (cdr options) nil nil (car options) - 'notmuch-address-history))))) + (funcall notmuch-address-selection-function + (format "Address (%s matches): " num-options) + (cdr options) (car options)))))) (if chosen (progn (push chosen notmuch-address-history) diff --git a/emacs/notmuch-crypto.el b/emacs/notmuch-crypto.el index 83e5d37a..52338249 100644 --- a/emacs/notmuch-crypto.el +++ b/emacs/notmuch-crypto.el @@ -19,6 +19,8 @@ ;; ;; Authors: Jameson Rollins <jrollins@finestructure.net> +(require 'notmuch-lib) + (defcustom notmuch-crypto-process-mime nil "Should cryptographic MIME parts be processed? @@ -76,7 +78,8 @@ mode." (define-button-type 'notmuch-crypto-status-button-type 'action (lambda (button) (message (button-get button 'help-echo))) 'follow-link t - 'help-echo "Set notmuch-crypto-process-mime to process cryptographic mime parts.") + 'help-echo "Set notmuch-crypto-process-mime to process cryptographic mime parts." + :supertype 'notmuch-button-type) (defun notmuch-crypto-insert-sigstatus-button (sigstatus from) (let* ((status (plist-get sigstatus :status)) diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index 6db62a01..c1c6f4b4 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -26,7 +26,7 @@ (require 'notmuch-lib) (require 'notmuch-mua) -(declare-function notmuch-search "notmuch" (query &optional oldest-first target-thread target-line continuation)) +(declare-function notmuch-search "notmuch" (&optional query oldest-first target-thread target-line continuation)) (declare-function notmuch-poll "notmuch" ()) (defcustom notmuch-hello-recent-searches-max 10 @@ -381,26 +381,38 @@ 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))) + (with-temp-buffer + (dolist (elem query-alist nil) + (let ((count-query (if (consp (cdr elem)) + ;; do we have a different query for the message count? + (third elem) + (cdr elem)))) + (insert + (notmuch-hello-filtered-query count-query + (or (plist-get options :filter-count) + (plist-get options :filter))) + "\n"))) + + (call-process-region (point-min) (point-max) notmuch-command + t t nil "count" "--batch") + (goto-char (point-min)) + + (notmuch-remove-if-not + #'identity + (mapcar + (lambda (elem) + (let ((name (car elem)) + (search-query (if (consp (cdr elem)) + ;; do we have a different query for the message count? + (second elem) + (cdr elem))) + (message-count (prog1 (read (current-buffer)) + (forward-line 1)))) + (and (or (plist-get options :show-empty-searches) (> message-count 0)) + (list name (notmuch-hello-filtered-query + search-query (plist-get options :filter)) + message-count)))) + query-alist)))) (defun notmuch-hello-insert-buttons (searches) "Insert buttons for SEARCHES. @@ -504,7 +516,7 @@ Complete list of currently available key bindings: (notmuch-remove-if-not (lambda (tag) (not (member tag hide-tags))) - (process-lines notmuch-command "search-tags")))) + (process-lines notmuch-command "search" "--output=tags" "*")))) (defun notmuch-hello-insert-header () "Insert the default notmuch-hello header." diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index d78bcf80..790136e0 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -97,13 +97,40 @@ For example, if you wanted to remove an \"inbox\" tag and add an :group 'notmuch-search :group 'notmuch-show) +;; By default clicking on a button does not select the window +;; containing the button (as opposed to clicking on a widget which +;; does). This means that the button action is then executed in the +;; current selected window which can cause problems if the button +;; changes the buffer (e.g., id: links) or moves point. +;; +;; This provides a button type which overrides mouse-action so that +;; the button's window is selected before the action is run. Other +;; notmuch buttons can get the same behaviour by inheriting from this +;; button type. +(define-button-type 'notmuch-button-type + 'mouse-action (lambda (button) + (select-window (posn-window (event-start last-input-event))) + (button-activate button))) + +(defun notmuch-command-to-string (&rest args) + "Synchronously invoke \"notmuch\" with the given list of arguments. + +If notmuch exits with a non-zero status, output from the process +will appear in a buffer named \"*Notmuch errors*\" and an error +will be signaled. + +Otherwise the output will be returned" + (with-temp-buffer + (let* ((status (apply #'call-process notmuch-command nil t nil args)) + (output (buffer-string))) + (notmuch-check-exit-status status (cons notmuch-command args) output) + output))) + (defun notmuch-version () "Return a string with the notmuch version number." (let ((long-string ;; Trim off the trailing newline. - (substring (shell-command-to-string - (concat notmuch-command " --version")) - 0 -1))) + (substring (notmuch-command-to-string "--version") 0 -1))) (if (string-match "^notmuch\\( version\\)? \\(.*\\)$" long-string) (match-string 2 long-string) @@ -112,9 +139,7 @@ For example, if you wanted to remove an \"inbox\" tag and add an (defun notmuch-config-get (item) "Return a value from the notmuch configuration." ;; Trim off the trailing newline - (substring (shell-command-to-string - (concat notmuch-command " config get " item)) - 0 -1)) + (substring (notmuch-command-to-string "config" "get" item) 0 -1)) (defun notmuch-database-path () "Return the database.path value from the notmuch configuration." @@ -188,19 +213,6 @@ user-friendly queries." (setq list (cdr list))) (nreverse out))) -;; This lets us avoid compiling these replacement functions when emacs -;; is sufficiently new enough to supply them alone. We do the macro -;; treatment rather than just wrapping our defun calls in a when form -;; specifically so that the compiler never sees the code on new emacs, -;; (since the code is triggering warnings that we don't know how to get -;; rid of. -;; -;; A more clever macro here would accept a condition and a list of forms. -(defmacro compile-on-emacs-prior-to-23 (form) - "Conditionally evaluate form only on emacs < emacs-23." - (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 "/")) @@ -301,20 +313,52 @@ current buffer, if possible." (loop for (key value . rest) on plist by #'cddr collect (cons (intern (substring (symbol-name key) 1)) value))) -(defun notmuch-combine-face-text-property (start end face) +(defun notmuch-face-ensure-list-form (face) + "Return FACE in face list form. + +If FACE is already a face list, it will be returned as-is. If +FACE is a face name or face plist, it will be returned as a +single element face list." + (if (and (listp face) (not (keywordp (car face)))) + face + (list face))) + +(defun notmuch-combine-face-text-property (start end face &optional below object) "Combine FACE into the 'face text property between START and END. This function combines FACE with any existing faces between START -and END. Attributes specified by FACE take precedence over -existing attributes. FACE must be a face name (a symbol or -string), a property list of face attributes, or a list of these." - - (let ((pos start)) +and END in OBJECT (which defaults to the current buffer). +Attributes specified by FACE take precedence over existing +attributes unless BELOW is non-nil. FACE must be a face name (a +symbol or string), a property list of face attributes, or a list +of these. For convenience when applied to strings, this returns +OBJECT." + + ;; A face property can have three forms: a face name (a string or + ;; symbol), a property list, or a list of these two forms. In the + ;; list case, the faces will be combined, with the earlier faces + ;; taking precedent. Here we canonicalize everything to list form + ;; to make it easy to combine. + (let ((pos start) + (face-list (notmuch-face-ensure-list-form face))) (while (< pos end) - (let ((cur (get-text-property pos 'face)) - (next (next-single-property-change pos 'face nil end))) - (put-text-property pos next 'face (cons face cur)) - (setq pos next))))) + (let* ((cur (get-text-property pos 'face object)) + (cur-list (notmuch-face-ensure-list-form cur)) + (new (cond ((null cur-list) face) + (below (append cur-list face-list)) + (t (append face-list cur-list)))) + (next (next-single-property-change pos 'face object end))) + (put-text-property pos next 'face new object) + (setq pos next)))) + object) + +(defun notmuch-combine-face-text-property-string (string face &optional below) + (notmuch-combine-face-text-property + 0 + (length string) + face + below + string)) (defun notmuch-logged-error (msg &optional extra) "Log MSG and EXTRA to *Notmuch errors* and signal MSG. @@ -425,29 +469,6 @@ an error." (json-read))) (delete-file err-file))))) -;; Compatibility functions for versions of emacs before emacs 23. -;; -;; Both functions here were copied from emacs 23 with the following copyright: -;; -;; Copyright (C) 1985, 1986, 1992, 1994, 1995, 1999, 2000, 2001, 2002, 2003, -;; 2004, 2005, 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc. -;; -;; and under the GPL version 3 (or later) exactly as notmuch itself. -(compile-on-emacs-prior-to-23 - (defun apply-partially (fun &rest args) - "Return a function that is a partial application of FUN to ARGS. -ARGS is a list of the first N arguments to pass to FUN. -The result is a new function which does the same as FUN, except that -the first N arguments are fixed at the values with which this function -was called." - (lexical-let ((fun fun) (args1 args)) - (lambda (&rest args2) (apply fun (append args1 args2)))))) - -(compile-on-emacs-prior-to-23 - (defun mouse-event-p (object) - "Return non-nil if OBJECT is a mouse click event." - (memq (event-basic-type object) '(mouse-1 mouse-2 mouse-3 mouse-movement)))) - ;; This variable is used only buffer local, but it needs to be ;; declared globally first to avoid compiler warnings. (defvar notmuch-show-process-crypto nil) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index 1864dd15..d56154eb 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -38,7 +38,6 @@ (require 'notmuch-print) (declare-function notmuch-call-notmuch-process "notmuch" (&rest args)) -(declare-function notmuch-fontify-headers "notmuch" nil) (declare-function notmuch-search-next-thread "notmuch" nil) (declare-function notmuch-search-show-thread "notmuch" nil) @@ -158,6 +157,7 @@ indentation." '(("Gmane" . "http://mid.gmane.org/") ("MARC" . "http://marc.info/?i=") ("Mail Archive, The" . "http://mail-archive.com/search?l=mid&q=") + ("LKML" . "http://lkml.kernel.org/r/") ;; FIXME: can these services be searched by `Message-Id' ? ;; ("MarkMail" . "http://markmail.org/") ;; ("Nabble" . "http://nabble.com/") @@ -362,8 +362,7 @@ operation on the contents of the current buffer." (if (re-search-forward "(\\([^()]*\\))$" (line-end-position) t) (let ((inhibit-read-only t)) (replace-match (concat "(" - (propertize (mapconcat 'identity tags " ") - 'face 'notmuch-tag-face) + (notmuch-tag-format-tags tags) ")")))))) (defun notmuch-clean-address (address) @@ -441,8 +440,7 @@ message at DEPTH in the current thread." " (" date ") (" - (propertize (mapconcat 'identity tags " ") - 'face 'notmuch-tag-face) + (notmuch-tag-format-tags tags) ")\n") (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face))) @@ -469,7 +467,8 @@ message at DEPTH in the current thread." 'action 'notmuch-show-part-button-default 'keymap 'notmuch-show-part-button-map 'follow-link t - 'face 'message-mml) + 'face 'message-mml + :supertype 'notmuch-button-type) (defvar notmuch-show-part-button-map (let ((map (make-sparse-keymap))) @@ -798,9 +797,9 @@ message at DEPTH in the current thread." (defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth declared-type) (notmuch-show-insert-part-text/calendar msg part content-type nth depth declared-type)) -(defun notmuch-show-insert-part-application/octet-stream (msg part content-type nth depth declared-type) +(defun notmuch-show-get-mime-type-of-application/octet-stream (part) ;; If we can deduce a MIME type from the filename of the attachment, - ;; do so and pass it on to the handler for that type. + ;; we return that. (if (plist-get part :filename) (let ((extension (file-name-extension (plist-get part :filename))) mime-type) @@ -810,13 +809,13 @@ message at DEPTH in the current thread." (setq mime-type (mailcap-extension-to-mime extension)) (if (and mime-type (not (string-equal mime-type "application/octet-stream"))) - (notmuch-show-insert-bodypart-internal msg part mime-type nth depth content-type) + mime-type nil)) nil)))) ;; Handler for wash generated inline patch fake parts. (defun notmuch-show-insert-part-inline-patch-fake-part (msg part content-type nth depth declared-type) - (notmuch-show-insert-part-*/* msg part "text/x-diff" nth depth "inline patch")) + (notmuch-show-insert-part-*/* msg part content-type nth depth declared-type)) (defun notmuch-show-insert-part-text/html (msg part content-type nth depth declared-type) ;; text/html handler to work around bugs in renderers and our @@ -887,11 +886,16 @@ message at DEPTH in the current thread." "Insert the body part PART at depth DEPTH in the current thread. If HIDE is non-nil then initially hide this part." - (let ((content-type (downcase (plist-get part :content-type))) - (nth (plist-get part :id)) - (beg (point))) - - (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type) + (let* ((content-type (downcase (plist-get part :content-type))) + (mime-type (or (and (string= content-type "application/octet-stream") + (notmuch-show-get-mime-type-of-application/octet-stream part)) + (and (string= content-type "inline patch") + "text/x-diff") + content-type)) + (nth (plist-get part :id)) + (beg (point))) + + (notmuch-show-insert-bodypart-internal msg part mime-type nth depth content-type) ;; Some of the body part handlers leave point somewhere up in the ;; part, so we make sure that we're down at the end. (goto-char (point-max)) @@ -1085,6 +1089,7 @@ buttons for a corresponding notmuch search." ;; Remove the overlay created by goto-address-mode (remove-overlays (first link) (second link) 'goto-address t) (make-text-button (first link) (second link) + :type 'notmuch-button-type 'action `(lambda (arg) (notmuch-show ,(third link))) 'follow-link t diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index 4fce3a98..064cfa8d 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -1,5 +1,6 @@ ;; notmuch-tag.el --- tag messages within emacs ;; +;; Copyright © Damien Cassou ;; Copyright © Carl Worth ;; ;; This file is part of Notmuch. @@ -18,11 +19,144 @@ ;; along with Notmuch. If not, see <http://www.gnu.org/licenses/>. ;; ;; Authors: Carl Worth <cworth@cworth.org> +;; Damien Cassou <damien.cassou@gmail.com> +;; +;;; Code: +;; -(eval-when-compile (require 'cl)) +(require 'cl) (require 'crm) (require 'notmuch-lib) +(defcustom notmuch-tag-formats + '(("unread" (propertize tag 'face '(:foreground "red"))) + ("flagged" (notmuch-tag-format-image-data tag (notmuch-tag-star-icon)))) + "Custom formats for individual tags. + +This gives a list that maps from tag names to lists of formatting +expressions. The car of each element gives a tag name and the +cdr gives a list of Elisp expressions that modify the tag. If +the list is empty, the tag will simply be hidden. Otherwise, +each expression will be evaluated in order: for the first +expression, the variable `tag' will be bound to the tag name; for +each later expression, the variable `tag' will be bound to the +result of the previous expression. In this way, each expression +can build on the formatting performed by the previous expression. +The result of the last expression will displayed in place of the +tag. + +For example, to replace a tag with another string, simply use +that string as a formatting expression. To change the foreground +of a tag to red, use the expression + (propertize tag 'face '(:foreground \"red\")) + +See also `notmuch-tag-format-image', which can help replace tags +with images." + + :group 'notmuch-search + :group 'notmuch-show + :type '(alist :key-type (string :tag "Tag") + :extra-offset -3 + :value-type + (radio :format "%v" + (const :tag "Hidden" nil) + (set :tag "Modified" + (string :tag "Display as") + (list :tag "Face" :extra-offset -4 + (const :format "" :inline t + (propertize tag 'face)) + (list :format "%v" + (const :format "" quote) + custom-face-edit)) + (list :format "%v" :extra-offset -4 + (const :format "" :inline t + (notmuch-tag-format-image-data tag)) + (choice :tag "Image" + (const :tag "Star" + (notmuch-tag-star-icon)) + (const :tag "Empty star" + (notmuch-tag-star-empty-icon)) + (const :tag "Tag" + (notmuch-tag-tag-icon)) + (string :tag "Custom"))) + (sexp :tag "Custom"))))) + +(defun notmuch-tag-format-image-data (tag data) + "Replace TAG with image DATA, if available. + +This function returns a propertized string that will display image +DATA in place of TAG.This is designed for use in +`notmuch-tag-formats'. + +DATA is the content of an SVG picture (e.g., as returned by +`notmuch-tag-star-icon')." + (propertize tag 'display + `(image :type svg + :data ,data + :ascent center + :mask heuristic))) + +(defun notmuch-tag-star-icon () + "Return SVG data representing a star icon. +This can be used with `notmuch-tag-format-image-data'." +"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?> +<svg version=\"1.1\" width=\"16\" height=\"16\"> + <g transform=\"translate(-242.81601,-315.59635)\"> + <path + d=\"m 290.25762,334.31206 -17.64143,-11.77975 -19.70508,7.85447 5.75171,-20.41814 -13.55925,-16.31348 21.19618,-0.83936 11.325,-17.93675 7.34825,19.89939 20.55849,5.22795 -16.65471,13.13786 z\" + transform=\"matrix(0.2484147,-0.02623394,0.02623394,0.2484147,174.63605,255.37691)\" + style=\"fill:#ffff00;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\" /> + </g> +</svg>") + +(defun notmuch-tag-star-empty-icon () + "Return SVG data representing an empty star icon. +This can be used with `notmuch-tag-format-image-data'." + "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?> +<svg version=\"1.1\" width=\"16\" height=\"16\"> + <g transform=\"translate(-242.81601,-315.59635)\"> + <path + d=\"m 290.25762,334.31206 -17.64143,-11.77975 -19.70508,7.85447 5.75171,-20.41814 -13.55925,-16.31348 21.19618,-0.83936 11.325,-17.93675 7.34825,19.89939 20.55849,5.22795 -16.65471,13.13786 z\" + transform=\"matrix(0.2484147,-0.02623394,0.02623394,0.2484147,174.63605,255.37691)\" + style=\"fill:#d6d6d1;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\" /> + </g> +</svg>") + +(defun notmuch-tag-tag-icon () + "Return SVG data representing a tag icon. +This can be used with `notmuch-tag-format-image-data'." + "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?> +<svg version=\"1.1\" width=\"16\" height=\"16\"> + <g transform=\"translate(0,-1036.3622)\"> + <path + d=\"m 0.44642857,1040.9336 12.50000043,0 2.700893,3.6161 -2.700893,3.616 -12.50000043,0 z\" + style=\"fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1\" /> + </g> +</svg>") + +(defun notmuch-tag-format-tag (tag) + "Format TAG by looking into `notmuch-tag-formats'." + (let ((formats (assoc tag notmuch-tag-formats))) + (cond + ((null formats) ;; - Tag not in `notmuch-tag-formats', + tag) ;; the format is the tag itself. + ((null (cdr formats)) ;; - Tag was deliberately hidden, + nil) ;; no format must be returned + (t ;; - Tag was found and has formats, + (let ((tag tag)) ;; we must apply all the formats. + (dolist (format (cdr formats) tag) + (setq tag (eval format)))))))) + +(defun notmuch-tag-format-tags (tags) + "Return a string representing formatted TAGS." + (notmuch-combine-face-text-property-string + (mapconcat #'identity + ;; nil indicated that the tag was deliberately hidden + (delq nil (mapcar #'notmuch-tag-format-tag tags)) + " ") + 'notmuch-tag-face + t)) + (defcustom notmuch-before-tag-hook nil "Hooks that are run before tags of a message are modified. @@ -158,3 +292,7 @@ begin with a \"+\" or a \"-\". If REVERSE is non-nil, replace all ;; (provide 'notmuch-tag) + +;; Local Variables: +;; byte-compile-warnings: (not cl-functions) +;; End: diff --git a/emacs/notmuch-wash.el b/emacs/notmuch-wash.el index d6db4fa2..8a68819c 100644 --- a/emacs/notmuch-wash.el +++ b/emacs/notmuch-wash.el @@ -23,7 +23,7 @@ (require 'coolj) -(declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth)) +(declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth &optional hide)) ;; @@ -115,7 +115,8 @@ lower).") (define-button-type 'notmuch-wash-button-invisibility-toggle-type 'action 'notmuch-wash-toggle-invisible-action 'follow-link t - 'face 'font-lock-comment-face) + 'face 'font-lock-comment-face + :supertype 'notmuch-button-type) (define-button-type 'notmuch-wash-button-citation-toggle-type 'help-echo "mouse-1, RET: Show citation" @@ -364,7 +365,7 @@ for error." (setq patch-end (match-beginning 0))) (save-restriction (narrow-to-region patch-start patch-end) - (setq part (plist-put part :content-type "inline-patch-fake-part")) + (setq part (plist-put part :content-type "inline patch")) (setq part (plist-put part :content (buffer-string))) (setq part (plist-put part :id -1)) (setq part (plist-put part :filename diff --git a/emacs/notmuch.el b/emacs/notmuch.el index c98a4feb..4c1a6cac 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -660,7 +660,7 @@ of the result." ;; things happen if a sentinel signals. Mimic ;; the top-level's handling of error messages. (error - (message "%s" (second err)) + (message "%s" (error-message-string err)) (throw 'return nil))) (if (and atbob (not (string= notmuch-search-target-thread "found"))) @@ -797,9 +797,8 @@ non-authors is found, assume that all of the authors match." (notmuch-search-insert-authors format-string (plist-get result :authors))) ((string-equal field "tags") - (let ((tags-str (mapconcat 'identity (plist-get result :tags) " "))) - (insert (propertize (format format-string tags-str) - 'face 'notmuch-tag-face)))))) + (let ((tags (plist-get result :tags))) + (insert (format format-string (notmuch-tag-format-tags tags))))))) (defun notmuch-search-show-result (result &optional pos) "Insert RESULT at POS or the end of the buffer if POS is null." diff --git a/lib/database.cc b/lib/database.cc index 91d43298..52ed618b 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -501,8 +501,10 @@ _parse_message_id (void *ctx, const char *message_id, const char **next) * 'message_id' in the result (to avoid mass confusion when a single * message references itself cyclically---and yes, mail messages are * not infrequent in the wild that do this---don't ask me why). -*/ -static void + * + * Return the last reference parsed, if it is not equal to message_id. + */ +static char * parse_references (void *ctx, const char *message_id, GHashTable *hash, @@ -511,7 +513,7 @@ parse_references (void *ctx, char *ref; if (refs == NULL || *refs == '\0') - return; + return NULL; while (*refs) { ref = _parse_message_id (ctx, refs, &refs); @@ -519,6 +521,17 @@ parse_references (void *ctx, if (ref && strcmp (ref, message_id)) g_hash_table_insert (hash, ref, NULL); } + + /* The return value of this function is used to add a parent + * reference to the database. We should avoid making a message + * its own parent, thus the following check. + */ + + if (ref && strcmp(ref, message_id)) { + return ref; + } else { + return NULL; + } } notmuch_status_t @@ -1510,28 +1523,33 @@ _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch, { GHashTable *parents = NULL; const char *refs, *in_reply_to, *in_reply_to_message_id; + const char *last_ref_message_id, *this_message_id; GList *l, *keys = NULL; notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; parents = g_hash_table_new_full (g_str_hash, g_str_equal, _my_talloc_free_for_g_hash, NULL); + this_message_id = notmuch_message_get_message_id (message); refs = notmuch_message_file_get_header (message_file, "references"); - parse_references (message, notmuch_message_get_message_id (message), - parents, refs); + last_ref_message_id = parse_references (message, + this_message_id, + parents, refs); in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to"); - parse_references (message, notmuch_message_get_message_id (message), - parents, in_reply_to); - - /* Carefully avoid adding any self-referential in-reply-to term. */ - in_reply_to_message_id = _parse_message_id (message, in_reply_to, NULL); - if (in_reply_to_message_id && - strcmp (in_reply_to_message_id, - notmuch_message_get_message_id (message))) - { + in_reply_to_message_id = parse_references (message, + this_message_id, + parents, in_reply_to); + + /* For the parent of this message, use the last message ID of the + * References header, if available. If not, fall back to the + * first message ID in the In-Reply-To header. */ + if (last_ref_message_id) { + _notmuch_message_add_term (message, "replyto", + last_ref_message_id); + } else if (in_reply_to_message_id) { _notmuch_message_add_term (message, "replyto", - _parse_message_id (message, in_reply_to, NULL)); + in_reply_to_message_id); } keys = g_hash_table_get_keys (parents); diff --git a/lib/message.cc b/lib/message.cc index 320901f7..c4261e61 100644 --- a/lib/message.cc +++ b/lib/message.cc @@ -266,18 +266,18 @@ _notmuch_message_get_term (notmuch_message_t *message, const char *prefix) { int prefix_len = strlen (prefix); - const char *term = NULL; char *value; i.skip_to (prefix); - if (i != end) - term = (*i).c_str (); + if (i == end) + return NULL; - if (!term || strncmp (term, prefix, prefix_len)) + std::string term = *i; + if (strncmp (term.c_str(), prefix, prefix_len)) return NULL; - value = talloc_strdup (message, term + prefix_len); + value = talloc_strdup (message, term.c_str() + prefix_len); #if DEBUG_DATABASE_SANITY i++; @@ -462,9 +462,9 @@ notmuch_message_get_thread_id (notmuch_message_t *message) void _notmuch_message_add_reply (notmuch_message_t *message, - notmuch_message_node_t *reply) + notmuch_message_t *reply) { - _notmuch_message_list_append (message->replies, reply); + _notmuch_message_list_add_message (message->replies, reply); } notmuch_messages_t * diff --git a/lib/messages.c b/lib/messages.c index 11218648..0eee5690 100644 --- a/lib/messages.c +++ b/lib/messages.c @@ -42,19 +42,7 @@ _notmuch_message_list_create (const void *ctx) return list; } -/* Append a single 'node' to the end of 'list'. - */ -void -_notmuch_message_list_append (notmuch_message_list_t *list, - notmuch_message_node_t *node) -{ - *(list->tail) = node; - list->tail = &node->next; -} - -/* Allocate a new node for 'message' and append it to the end of - * 'list'. - */ +/* Append 'message' to the end of 'list'. */ void _notmuch_message_list_add_message (notmuch_message_list_t *list, notmuch_message_t *message) @@ -64,7 +52,8 @@ _notmuch_message_list_add_message (notmuch_message_list_t *list, node->message = message; node->next = NULL; - _notmuch_message_list_append (list, node); + *(list->tail) = node; + list->tail = &node->next; } notmuch_messages_t * diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h index 7a409f54..cc55bb9d 100644 --- a/lib/notmuch-private.h +++ b/lib/notmuch-private.h @@ -236,6 +236,7 @@ _notmuch_thread_create (void *ctx, unsigned int seed_doc_id, notmuch_doc_id_set_t *match_set, notmuch_string_list_t *excluded_terms, + notmuch_exclude_t omit_exclude, notmuch_sort_t sort); /* message.cc */ @@ -429,10 +430,6 @@ notmuch_message_list_t * _notmuch_message_list_create (const void *ctx); void -_notmuch_message_list_append (notmuch_message_list_t *list, - notmuch_message_node_t *node); - -void _notmuch_message_list_add_message (notmuch_message_list_t *list, notmuch_message_t *message); @@ -462,7 +459,7 @@ _notmuch_doc_id_set_remove (notmuch_doc_id_set_t *doc_ids, void _notmuch_message_add_reply (notmuch_message_t *message, - notmuch_message_node_t *reply); + notmuch_message_t *reply); /* sha1.c */ diff --git a/lib/notmuch.h b/lib/notmuch.h index 3633bedd..27b43ff6 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -500,14 +500,22 @@ typedef enum { const char * notmuch_query_get_query_string (notmuch_query_t *query); +/* Exclude values for notmuch_query_set_omit_excluded */ +typedef enum { + NOTMUCH_EXCLUDE_FALSE, + NOTMUCH_EXCLUDE_TRUE, + NOTMUCH_EXCLUDE_ALL +} notmuch_exclude_t; + /* 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. + * If set to TRUE or ALL, notmuch_query_search_messages will omit excluded + * messages from the results, and notmuch_query_search_threads will omit + * threads that match only in excluded messages. If set to TRUE, + * notmuch_query_search_threads will include all messages in threads that + * match in at least one non-excluded message. Otherwise, if set to ALL, + * notmuch_query_search_threads will omit excluded messages from all threads. * * The performance difference when calling * notmuch_query_search_messages should be relatively small (and both @@ -516,9 +524,9 @@ notmuch_query_get_query_string (notmuch_query_t *query); * 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); +notmuch_query_set_omit_excluded (notmuch_query_t *query, + notmuch_exclude_t omit_excluded); /* Specify the sorting desired for this query. */ void @@ -719,20 +727,21 @@ int notmuch_thread_get_total_messages (notmuch_thread_t *thread); /* Get a notmuch_messages_t iterator for the top-level messages in - * 'thread'. + * 'thread' in oldest-first order. * * This iterator will not necessarily iterate over all of the messages * in the thread. It will only iterate over the messages in the thread * which are not replies to other messages in the thread. - * - * To iterate over all messages in the thread, the caller will need to - * iterate over the result of notmuch_message_get_replies for each - * top-level message (and do that recursively for the resulting - * messages, etc.). */ notmuch_messages_t * notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread); +/* Get a notmuch_thread_t iterator for all messages in 'thread' in + * oldest-first order. + */ +notmuch_messages_t * +notmuch_thread_get_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 diff --git a/lib/query.cc b/lib/query.cc index e9c1a2d1..1cc768f8 100644 --- a/lib/query.cc +++ b/lib/query.cc @@ -28,7 +28,7 @@ struct _notmuch_query { const char *query_string; notmuch_sort_t sort; notmuch_string_list_t *exclude_terms; - notmuch_bool_t omit_excluded; + notmuch_exclude_t omit_excluded; }; typedef struct _notmuch_mset_messages { @@ -39,12 +39,12 @@ typedef struct _notmuch_mset_messages { } notmuch_mset_messages_t; struct _notmuch_doc_id_set { - unsigned int *bitmap; + unsigned char *bitmap; unsigned int bound; }; -#define DOCIDSET_WORD(bit) ((bit) / sizeof (unsigned int)) -#define DOCIDSET_BIT(bit) ((bit) % sizeof (unsigned int)) +#define DOCIDSET_WORD(bit) ((bit) / CHAR_BIT) +#define DOCIDSET_BIT(bit) ((bit) % CHAR_BIT) struct visible _notmuch_threads { notmuch_query_t *query; @@ -92,7 +92,7 @@ notmuch_query_create (notmuch_database_t *notmuch, query->exclude_terms = _notmuch_string_list_create (query); - query->omit_excluded = TRUE; + query->omit_excluded = NOTMUCH_EXCLUDE_TRUE; return query; } @@ -104,7 +104,8 @@ notmuch_query_get_query_string (notmuch_query_t *query) } void -notmuch_query_set_omit_excluded (notmuch_query_t *query, notmuch_bool_t omit_excluded) +notmuch_query_set_omit_excluded (notmuch_query_t *query, + notmuch_exclude_t omit_excluded) { query->omit_excluded = omit_excluded; } @@ -220,7 +221,7 @@ notmuch_query_search_messages (notmuch_query_t *query) if (query->exclude_terms) { exclude_query = _notmuch_exclude_tags (query, final_query); - if (query->omit_excluded) + if (query->omit_excluded != NOTMUCH_EXCLUDE_FALSE) final_query = Xapian::Query (Xapian::Query::OP_AND_NOT, final_query, exclude_query); else { @@ -359,11 +360,11 @@ _notmuch_doc_id_set_init (void *ctx, GArray *arr) { unsigned int max = 0; - unsigned int *bitmap; + unsigned char *bitmap; for (unsigned int i = 0; i < arr->len; i++) max = MAX(max, g_array_index (arr, unsigned int, i)); - bitmap = talloc_zero_array (ctx, unsigned int, 1 + max / sizeof (*bitmap)); + bitmap = talloc_zero_array (ctx, unsigned char, DOCIDSET_WORD(max) + 1); if (bitmap == NULL) return FALSE; @@ -486,6 +487,7 @@ notmuch_threads_get (notmuch_threads_t *threads) doc_id, &threads->match_set, threads->query->exclude_terms, + threads->query->omit_excluded, threads->query->sort); } diff --git a/lib/thread.cc b/lib/thread.cc index e976d643..bc078778 100644 --- a/lib/thread.cc +++ b/lib/thread.cc @@ -35,7 +35,11 @@ struct visible _notmuch_thread { char *authors; GHashTable *tags; + /* All messages, oldest first. */ notmuch_message_list_t *message_list; + /* Top-level messages, oldest first. */ + notmuch_message_list_t *toplevel_list; + GHashTable *message_hash; int total_messages; int matched_messages; @@ -186,8 +190,16 @@ _thread_cleanup_author (notmuch_thread_t *thread, if (comma && strlen(comma) > 1) { /* let's assemble what we think is the correct name */ lname = comma - author; - fname = strlen(author) - lname - 2; - strncpy(clean_author, comma + 2, fname); + + /* Skip all the spaces after the comma */ + fname = strlen(author) - lname - 1; + comma += 1; + while (*comma == ' ') { + fname -= 1; + comma += 1; + } + strncpy(clean_author, comma, fname); + *(clean_author+fname) = ' '; strncpy(clean_author + fname + 1, author, lname); *(clean_author+fname+1+lname) = '\0'; @@ -215,7 +227,8 @@ _thread_cleanup_author (notmuch_thread_t *thread, static void _thread_add_message (notmuch_thread_t *thread, notmuch_message_t *message, - notmuch_string_list_t *exclude_terms) + notmuch_string_list_t *exclude_terms, + notmuch_exclude_t omit_exclude) { notmuch_tags_t *tags; const char *tag; @@ -223,6 +236,28 @@ _thread_add_message (notmuch_thread_t *thread, InternetAddress *address; const char *from, *author; char *clean_author; + notmuch_bool_t message_excluded = FALSE; + + for (tags = notmuch_message_get_tags (message); + notmuch_tags_valid (tags); + notmuch_tags_move_to_next (tags)) + { + tag = notmuch_tags_get (tags); + /* Is message excluded? */ + for (notmuch_string_node_t *term = exclude_terms->head; + term != NULL; + term = term->next) + { + /* We ignore initial 'K'. */ + if (strcmp(tag, (term->string + 1)) == 0) { + message_excluded = TRUE; + break; + } + } + } + + if (message_excluded && omit_exclude == NOTMUCH_EXCLUDE_ALL) + return; _notmuch_message_list_add_message (thread->message_list, talloc_steal (thread, message)); @@ -263,17 +298,12 @@ _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); } + + /* Mark excluded messages. */ + if (message_excluded) + notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, TRUE); } static void @@ -345,29 +375,22 @@ _thread_add_matched_message (notmuch_thread_t *thread, } static void -_resolve_thread_relationships (unused (notmuch_thread_t *thread)) +_resolve_thread_relationships (notmuch_thread_t *thread) { - notmuch_message_node_t **prev, *node; + notmuch_message_node_t *node; notmuch_message_t *message, *parent; const char *in_reply_to; - prev = &thread->message_list->head; - while ((node = *prev)) { + for (node = thread->message_list->head; node; node = node->next) { message = node->message; in_reply_to = _notmuch_message_get_in_reply_to (message); if (in_reply_to && strlen (in_reply_to) && g_hash_table_lookup_extended (thread->message_hash, in_reply_to, NULL, (void **) &parent)) - { - *prev = node->next; - if (thread->message_list->tail == &node->next) - thread->message_list->tail = prev; - node->next = NULL; - _notmuch_message_add_reply (parent, node); - } else { - prev = &((*prev)->next); - } + _notmuch_message_add_reply (parent, message); + else + _notmuch_message_list_add_message (thread->toplevel_list, message); } /* XXX: After scanning through the entire list looking for parents @@ -404,9 +427,11 @@ _notmuch_thread_create (void *ctx, unsigned int seed_doc_id, notmuch_doc_id_set_t *match_set, notmuch_string_list_t *exclude_terms, + notmuch_exclude_t omit_excluded, notmuch_sort_t sort) { - notmuch_thread_t *thread; + void *local = talloc_new (ctx); + notmuch_thread_t *thread = NULL; notmuch_message_t *seed_message; const char *thread_id; char *thread_id_query_string; @@ -415,24 +440,23 @@ _notmuch_thread_create (void *ctx, notmuch_messages_t *messages; notmuch_message_t *message; - seed_message = _notmuch_message_create (ctx, notmuch, seed_doc_id, NULL); + seed_message = _notmuch_message_create (local, notmuch, seed_doc_id, NULL); if (! seed_message) INTERNAL_ERROR ("Thread seed message %u does not exist", seed_doc_id); thread_id = notmuch_message_get_thread_id (seed_message); - thread_id_query_string = talloc_asprintf (ctx, "thread:%s", thread_id); + thread_id_query_string = talloc_asprintf (local, "thread:%s", thread_id); if (unlikely (thread_id_query_string == NULL)) - return NULL; + goto DONE; - thread_id_query = notmuch_query_create (notmuch, thread_id_query_string); + thread_id_query = talloc_steal ( + local, notmuch_query_create (notmuch, thread_id_query_string)); if (unlikely (thread_id_query == NULL)) - return NULL; + goto DONE; - talloc_free (thread_id_query_string); - - thread = talloc (ctx, notmuch_thread_t); + thread = talloc (local, notmuch_thread_t); if (unlikely (thread == NULL)) - return NULL; + goto DONE; talloc_set_destructor (thread, _notmuch_thread_destructor); @@ -451,8 +475,12 @@ _notmuch_thread_create (void *ctx, free, NULL); thread->message_list = _notmuch_message_list_create (thread); - if (unlikely (thread->message_list == NULL)) - return NULL; + thread->toplevel_list = _notmuch_message_list_create (thread); + if (unlikely (thread->message_list == NULL || + thread->toplevel_list == NULL)) { + thread = NULL; + goto DONE; + } thread->message_hash = g_hash_table_new_full (g_str_hash, g_str_equal, free, NULL); @@ -479,7 +507,7 @@ _notmuch_thread_create (void *ctx, if (doc_id == seed_doc_id) message = seed_message; - _thread_add_message (thread, message, exclude_terms); + _thread_add_message (thread, message, exclude_terms, omit_excluded); if ( _notmuch_doc_id_set_contains (match_set, doc_id)) { _notmuch_doc_id_set_remove (match_set, doc_id); @@ -489,18 +517,27 @@ _notmuch_thread_create (void *ctx, _notmuch_message_close (message); } - notmuch_query_destroy (thread_id_query); - _resolve_thread_authors_string (thread); _resolve_thread_relationships (thread); + /* Commit to returning thread. */ + talloc_steal (ctx, thread); + + DONE: + talloc_free (local); return thread; } notmuch_messages_t * notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread) { + return _notmuch_messages_create (thread->toplevel_list); +} + +notmuch_messages_t * +notmuch_thread_get_messages (notmuch_thread_t *thread) +{ return _notmuch_messages_create (thread->message_list); } diff --git a/man/man1/notmuch-count.1 b/man/man1/notmuch-count.1 index 86a67fe2..7fc4378a 100644 --- a/man/man1/notmuch-count.1 +++ b/man/man1/notmuch-count.1 @@ -46,6 +46,26 @@ Output the number of matching threads. Specify whether to omit messages matching search.tag_exclude from the count (the default) or not. .RE + +.RS 4 +.TP 4 +.BR \-\-batch + +Read queries from a file (stdin by default), one per line, and output +the number of matching messages (or threads) to stdout, one per +line. On an empty input line the count of all messages (or threads) in +the database will be output. This option is not compatible with +specifying search terms on the command line. +.RE + +.RS 4 +.TP 4 +.BR "\-\-input=" <filename> + +Read input from given file, instead of from stdin. Implies +.BR --batch . +.RE + .RE .RE diff --git a/man/man1/notmuch-reply.1 b/man/man1/notmuch-reply.1 index 454bdee3..bf2021f5 100644 --- a/man/man1/notmuch-reply.1 +++ b/man/man1/notmuch-reply.1 @@ -89,9 +89,12 @@ user's addresses. Decrypt any MIME encrypted parts found in the selected content (ie. "multipart/encrypted" parts). Status of the decryption will be -reported (currently only supported with --format=json and --format=sexp) -and the multipart/encrypted part will be replaced by the decrypted -content. +reported (currently only supported with --format=json and +--format=sexp) and on successful decryption the multipart/encrypted +part will be replaced by the decrypted content. + +Decryption expects a functioning \fBgpg-agent\fR(1) to provide any +needed credentials. Without one, the decryption will fail. .RE See \fBnotmuch-search-terms\fR(7) diff --git a/man/man1/notmuch-search.1 b/man/man1/notmuch-search.1 index d3391f83..1c1e049b 100644 --- a/man/man1/notmuch-search.1 +++ b/man/man1/notmuch-search.1 @@ -130,15 +130,32 @@ Limit the number of displayed results to N. .RS 4 .TP 4 -.BR \-\-exclude=(true|false|flag) +.BR \-\-exclude=(true|false|all|flag) + +A message is called "excluded" if it matches at least one tag in +search.tag_exclude that does not appear explicitly in the search terms. +This option specifies whether to omit excluded messages in the search +process. + +The default value, +.BR true , +prevents excluded messages from matching the search terms. + +.B all +additionally prevents excluded messages from appearing in displayed +results, in effect behaving as though the excluded messages do not exist. + +.B false +allows excluded messages to match search terms and appear in displayed +results. Excluded messages are still marked in the relevant outputs. -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. +.BR --output=summary . +The output is almost identical to +.BR false , +but the "match count" is the number of matching non-excluded messages in the +thread, rather than the number of matching messages. .RE .SH EXIT STATUS diff --git a/man/man1/notmuch-show.1 b/man/man1/notmuch-show.1 index 8be9eaec..7697dfcc 100644 --- a/man/man1/notmuch-show.1 +++ b/man/man1/notmuch-show.1 @@ -163,8 +163,13 @@ will be replaced by the signed data. Decrypt any MIME encrypted parts found in the selected content (ie. "multipart/encrypted" parts). Status of the decryption will be reported (currently only supported with --format=json and ---format=sexp) and the multipart/encrypted part will be replaced -by the decrypted content. Implies --verify. +--format=sexp) and on successful decryption the multipart/encrypted +part will be replaced by the decrypted content. + +Decryption expects a functioning \fBgpg-agent\fR(1) to provide any +needed credentials. Without one, the decryption will fail. + +Implies --verify. .RE .RS 4 diff --git a/man/man1/notmuch-tag.1 b/man/man1/notmuch-tag.1 index 0052ca21..c7d7a4d3 100644 --- a/man/man1/notmuch-tag.1 +++ b/man/man1/notmuch-tag.1 @@ -4,7 +4,7 @@ notmuch-tag \- add/remove tags for all messages matching the search terms .SH SYNOPSIS .B notmuch tag -.RI "+<" tag ">|\-<" tag "> [...] [\-\-] <" search-term "> [...]" +.RI [ options "...] +<" tag ">|\-<" tag "> [...] [\-\-] <" search-term "> [...]" .B notmuch tag .RI "--batch" @@ -40,6 +40,16 @@ Supported options for include .RS 4 .TP 4 +.BR \-\-remove\-all + +Remove all tags from each message matching the search terms before +applying the tag changes appearing on the command line. This means +setting the tags of each message to the tags to be added. If there are +no tags to be added, the messages will have no tags. +.RE + +.RS 4 +.TP 4 .BR \-\-batch Read batch tagging operations from a file (stdin by default). This is more diff --git a/man/man1/notmuch.1 b/man/man1/notmuch.1 index a6573084..033cc107 100644 --- a/man/man1/notmuch.1 +++ b/man/man1/notmuch.1 @@ -21,7 +21,7 @@ notmuch \- thread-based email index, search, and tagging .SH SYNOPSIS .B notmuch -.IR command " [" args " ...]" +.RI "[" option " ...] " command " [" arg " ...]" .SH DESCRIPTION Notmuch is a command-line based program for indexing, searching, reading, and tagging large collections of email messages. @@ -50,6 +50,35 @@ interfaces to notmuch. The emacs-based interface to notmuch (available under in the Notmuch source distribution) is probably the most widely used at this time. +.SH OPTIONS + +Supported global options for +.B notmuch +include + +.RS 4 +.TP 4 +.B \-\-help + +Print a synopsis of available commands and exit. +.RE + +.RS 4 +.TP 4 +.B \-\-version + +Print the installed version of notmuch, and exit. +.RE + +.RS 4 +.TP 4 +.B \-\-config=FILE + +Specify the configuration file to use. This overrides any +configuration file specified by ${NOTMUCH_CONFIG}. + +.RE + .SH COMMANDS @@ -127,6 +156,19 @@ behavior of notmuch. .B NOTMUCH_CONFIG Specifies the location of the notmuch configuration file. Notmuch will use ${HOME}/.notmuch\-config if this variable is not set. + +.TP +.B NOTMUCH_TALLOC_REPORT +Location to write a talloc memory usage report. See +.B talloc_enable_leak_report_full +in \fBtalloc\fR(3) +for more information. + +.TP +.B NOTMUCH_DEBUG_QUERY +If set to a non-empty value, the notmuch library will print (to stderr) Xapian +queries it constructs. + .SH SEE ALSO \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1), diff --git a/mime-node.c b/mime-node.c index 839737a8..fd9e4a45 100644 --- a/mime-node.c +++ b/mime-node.c @@ -130,26 +130,166 @@ DONE: } #ifdef GMIME_ATLEAST_26 + +/* Signature list destructor (GMime 2.6) */ static int _signature_list_free (GMimeSignatureList **proxy) { g_object_unref (*proxy); return 0; } -#else + +/* Set up signature list destructor (GMime 2.6) */ +static void +set_signature_list_destructor (mime_node_t *node) +{ + GMimeSignatureList **proxy = talloc (node, GMimeSignatureList *); + if (proxy) { + *proxy = node->sig_list; + talloc_set_destructor (proxy, _signature_list_free); + } +} + +/* Verify a signed mime node (GMime 2.6) */ +static void +node_verify (mime_node_t *node, GMimeObject *part, + notmuch_crypto_context_t *cryptoctx) +{ + GError *err = NULL; + + node->verify_attempted = TRUE; + node->sig_list = g_mime_multipart_signed_verify + (GMIME_MULTIPART_SIGNED (part), cryptoctx, &err); + + if (node->sig_list) + set_signature_list_destructor (node); + else + fprintf (stderr, "Failed to verify signed part: %s\n", + err ? err->message : "no error explanation given"); + + if (err) + g_error_free (err); +} + +/* Decrypt and optionally verify an encrypted mime node (GMime 2.6) */ +static void +node_decrypt_and_verify (mime_node_t *node, GMimeObject *part, + notmuch_crypto_context_t *cryptoctx) +{ + GError *err = NULL; + GMimeDecryptResult *decrypt_result = NULL; + GMimeMultipartEncrypted *encrypteddata = GMIME_MULTIPART_ENCRYPTED (part); + + node->decrypt_attempted = TRUE; + node->decrypted_child = g_mime_multipart_encrypted_decrypt + (encrypteddata, cryptoctx, &decrypt_result, &err); + if (! node->decrypted_child) { + fprintf (stderr, "Failed to decrypt part: %s\n", + err ? err->message : "no error explanation given"); + goto DONE; + } + + node->decrypt_success = TRUE; + node->verify_attempted = TRUE; + + /* This may be NULL if the part is not signed. */ + node->sig_list = g_mime_decrypt_result_get_signatures (decrypt_result); + if (node->sig_list) { + g_object_ref (node->sig_list); + set_signature_list_destructor (node); + } + g_object_unref (decrypt_result); + + DONE: + if (err) + g_error_free (err); +} + +#else /* GMIME_ATLEAST_26 */ + +/* Signature validity destructor (GMime 2.4) */ static int _signature_validity_free (GMimeSignatureValidity **proxy) { g_mime_signature_validity_free (*proxy); return 0; } -#endif + +/* Set up signature validity destructor (GMime 2.4) */ +static void +set_signature_validity_destructor (mime_node_t *node, + GMimeSignatureValidity *sig_validity) +{ + GMimeSignatureValidity **proxy = talloc (node, GMimeSignatureValidity *); + if (proxy) { + *proxy = sig_validity; + talloc_set_destructor (proxy, _signature_validity_free); + } +} + +/* Verify a signed mime node (GMime 2.4) */ +static void +node_verify (mime_node_t *node, GMimeObject *part, + notmuch_crypto_context_t *cryptoctx) +{ + GError *err = NULL; + GMimeSignatureValidity *sig_validity; + + node->verify_attempted = TRUE; + sig_validity = g_mime_multipart_signed_verify + (GMIME_MULTIPART_SIGNED (part), cryptoctx, &err); + node->sig_validity = sig_validity; + if (sig_validity) { + set_signature_validity_destructor (node, sig_validity); + } else { + fprintf (stderr, "Failed to verify signed part: %s\n", + err ? err->message : "no error explanation given"); + } + + if (err) + g_error_free (err); +} + +/* Decrypt and optionally verify an encrypted mime node (GMime 2.4) */ +static void +node_decrypt_and_verify (mime_node_t *node, GMimeObject *part, + notmuch_crypto_context_t *cryptoctx) +{ + GError *err = NULL; + GMimeMultipartEncrypted *encrypteddata = GMIME_MULTIPART_ENCRYPTED (part); + + node->decrypt_attempted = TRUE; + node->decrypted_child = g_mime_multipart_encrypted_decrypt + (encrypteddata, cryptoctx, &err); + if (! node->decrypted_child) { + fprintf (stderr, "Failed to decrypt part: %s\n", + err ? err->message : "no error explanation given"); + goto DONE; + } + + node->decrypt_success = TRUE; + node->verify_attempted = TRUE; + + /* The GMimeSignatureValidity returned here is a const, unlike the + * one returned by g_mime_multipart_signed_verify() in + * node_verify() above, so the destructor is not needed. + */ + node->sig_validity = g_mime_multipart_encrypted_get_signature_validity (encrypteddata); + if (! node->sig_validity) + fprintf (stderr, "Failed to verify encrypted signed part: %s\n", + err ? err->message : "no error explanation given"); + + DONE: + if (err) + g_error_free (err); +} + +#endif /* GMIME_ATLEAST_26 */ static mime_node_t * _mime_node_create (mime_node_t *parent, GMimeObject *part) { mime_node_t *node = talloc_zero (parent, mime_node_t); - GError *err = NULL; notmuch_crypto_context_t *cryptoctx = NULL; /* Set basic node properties */ @@ -198,32 +338,7 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part) "message (must be exactly 2)\n", node->nchildren); } else { - GMimeMultipartEncrypted *encrypteddata = - GMIME_MULTIPART_ENCRYPTED (part); - node->decrypt_attempted = TRUE; -#ifdef GMIME_ATLEAST_26 - GMimeDecryptResult *decrypt_result = NULL; - node->decrypted_child = g_mime_multipart_encrypted_decrypt - (encrypteddata, cryptoctx, &decrypt_result, &err); -#else - node->decrypted_child = g_mime_multipart_encrypted_decrypt - (encrypteddata, cryptoctx, &err); -#endif - if (node->decrypted_child) { - node->decrypt_success = node->verify_attempted = TRUE; -#ifdef GMIME_ATLEAST_26 - /* This may be NULL if the part is not signed. */ - node->sig_list = g_mime_decrypt_result_get_signatures (decrypt_result); - if (node->sig_list) - g_object_ref (node->sig_list); - g_object_unref (decrypt_result); -#else - node->sig_validity = g_mime_multipart_encrypted_get_signature_validity (encrypteddata); -#endif - } else { - fprintf (stderr, "Failed to decrypt part: %s\n", - (err ? err->message : "no error explanation given")); - } + node_decrypt_and_verify (node, part, cryptoctx); } } else if (GMIME_IS_MULTIPART_SIGNED (part) && node->ctx->crypto->verify && cryptoctx) { if (node->nchildren != 2) { @@ -232,56 +347,10 @@ _mime_node_create (mime_node_t *parent, GMimeObject *part) "(must be exactly 2)\n", node->nchildren); } else { -#ifdef GMIME_ATLEAST_26 - node->sig_list = g_mime_multipart_signed_verify - (GMIME_MULTIPART_SIGNED (part), cryptoctx, &err); - node->verify_attempted = TRUE; - - if (!node->sig_list) - fprintf (stderr, "Failed to verify signed part: %s\n", - (err ? err->message : "no error explanation given")); -#else - /* For some reason the GMimeSignatureValidity returned - * here is not a const (inconsistent with that - * returned by - * g_mime_multipart_encrypted_get_signature_validity, - * and therefore needs to be properly disposed of. - * - * In GMime 2.6, they're both non-const, so we'll be able - * to clean up this asymmetry. */ - GMimeSignatureValidity *sig_validity = g_mime_multipart_signed_verify - (GMIME_MULTIPART_SIGNED (part), cryptoctx, &err); - node->verify_attempted = TRUE; - node->sig_validity = sig_validity; - if (sig_validity) { - GMimeSignatureValidity **proxy = - talloc (node, GMimeSignatureValidity *); - *proxy = sig_validity; - talloc_set_destructor (proxy, _signature_validity_free); - } -#endif + node_verify (node, part, cryptoctx); } } -#ifdef GMIME_ATLEAST_26 - /* sig_list may be created in both above cases, so we need to - * cleanly handle it here. */ - if (node->sig_list) { - GMimeSignatureList **proxy = talloc (node, GMimeSignatureList *); - *proxy = node->sig_list; - talloc_set_destructor (proxy, _signature_list_free); - } -#endif - -#ifndef GMIME_ATLEAST_26 - if (node->verify_attempted && !node->sig_validity) - fprintf (stderr, "Failed to verify signed part: %s\n", - (err ? err->message : "no error explanation given")); -#endif - - if (err) - g_error_free (err); - return node; } diff --git a/notmuch-client.h b/notmuch-client.h index 5f288368..45749a6b 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -150,6 +150,8 @@ chomp_newline (char *str) */ extern int notmuch_format_version; +typedef struct _notmuch_config notmuch_config_t; + /* Commands that support structured output should support the * following argument * { NOTMUCH_OPT_INT, ¬much_format_version, "format-version", 0, 0 } @@ -169,40 +171,34 @@ int notmuch_crypto_cleanup (notmuch_crypto_t *crypto); int -notmuch_count_command (void *ctx, int argc, char *argv[]); - -int -notmuch_dump_command (void *ctx, int argc, char *argv[]); - -int -notmuch_new_command (void *ctx, int argc, char *argv[]); +notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_reply_command (void *ctx, int argc, char *argv[]); +notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_restore_command (void *ctx, int argc, char *argv[]); +notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_search_command (void *ctx, int argc, char *argv[]); +notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_setup_command (void *ctx, int argc, char *argv[]); +notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_show_command (void *ctx, int argc, char *argv[]); +notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_tag_command (void *ctx, int argc, char *argv[]); +notmuch_setup_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_search_tags_command (void *ctx, int argc, char *argv[]); +notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_cat_command (void *ctx, int argc, char *argv[]); +notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]); int -notmuch_config_command (void *ctx, int argc, char *argv[]); +notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]); const char * notmuch_time_relative_date (const void *ctx, time_t then); @@ -243,12 +239,10 @@ json_quote_str (const void *ctx, const char *str); /* notmuch-config.c */ -typedef struct _notmuch_config notmuch_config_t; - notmuch_config_t * notmuch_config_open (void *ctx, const char *filename, - notmuch_bool_t *is_new_ret); + notmuch_bool_t create_new); void notmuch_config_close (notmuch_config_t *config); @@ -256,6 +250,9 @@ notmuch_config_close (notmuch_config_t *config); int notmuch_config_save (notmuch_config_t *config); +notmuch_bool_t +notmuch_config_is_new (notmuch_config_t *config); + const char * notmuch_config_get_database_path (notmuch_config_t *config); diff --git a/notmuch-config.c b/notmuch-config.c index b5c2066e..befe9b5b 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -104,6 +104,7 @@ static const char search_config_comment[] = struct _notmuch_config { char *filename; GKeyFile *key_file; + notmuch_bool_t is_new; char *database_path; char *user_name; @@ -232,10 +233,9 @@ get_username_from_passwd_file (void *ctx) notmuch_config_t * notmuch_config_open (void *ctx, const char *filename, - notmuch_bool_t *is_new_ret) + notmuch_bool_t create_new) { GError *error = NULL; - int is_new = 0; size_t tmp; char *notmuch_config_env = NULL; int file_had_database_group; @@ -244,9 +244,6 @@ notmuch_config_open (void *ctx, int file_had_maildir_group; int file_had_search_group; - if (is_new_ret) - *is_new_ret = 0; - notmuch_config_t *config = talloc (ctx, notmuch_config_t); if (config == NULL) { fprintf (stderr, "Out of memory.\n"); @@ -266,6 +263,7 @@ notmuch_config_open (void *ctx, config->key_file = g_key_file_new (); + config->is_new = FALSE; config->database_path = NULL; config->user_name = NULL; config->user_primary_email = NULL; @@ -284,17 +282,16 @@ notmuch_config_open (void *ctx, G_KEY_FILE_KEEP_COMMENTS, &error)) { - /* If the caller passed a non-NULL value for is_new_ret, then - * the caller is prepared for a default configuration file in - * the case of FILE NOT FOUND. Otherwise, any read failure is - * an error. + /* If create_new is true, then the caller is prepared for a + * default configuration file in the case of FILE NOT + * FOUND. Otherwise, any read failure is an error. */ - if (is_new_ret && + if (create_new && error->domain == G_FILE_ERROR && error->code == G_FILE_ERROR_NOENT) { g_error_free (error); - is_new = 1; + config->is_new = TRUE; } else { @@ -377,7 +374,7 @@ notmuch_config_open (void *ctx, } if (notmuch_config_get_search_exclude_tags (config, &tmp) == NULL) { - if (is_new) { + if (config->is_new) { const char *tags[] = { "deleted", "spam" }; notmuch_config_set_search_exclude_tags (config, tags, 2); } else { @@ -397,43 +394,29 @@ notmuch_config_open (void *ctx, /* Whenever we know of configuration sections that don't appear in * the configuration file, we add some comments to help the user * understand what can be done. */ - if (is_new) - { + if (config->is_new) g_key_file_set_comment (config->key_file, NULL, NULL, toplevel_config_comment, NULL); - } if (! file_had_database_group) - { g_key_file_set_comment (config->key_file, "database", NULL, database_config_comment, NULL); - } if (! file_had_new_group) - { g_key_file_set_comment (config->key_file, "new", NULL, new_config_comment, NULL); - } if (! file_had_user_group) - { g_key_file_set_comment (config->key_file, "user", NULL, user_config_comment, NULL); - } if (! file_had_maildir_group) - { g_key_file_set_comment (config->key_file, "maildir", NULL, maildir_config_comment, NULL); - } - if (! file_had_search_group) { + if (! file_had_search_group) g_key_file_set_comment (config->key_file, "search", NULL, search_config_comment, NULL); - } - - if (is_new_ret) - *is_new_ret = is_new; return config; } @@ -461,7 +444,7 @@ int notmuch_config_save (notmuch_config_t *config) { size_t length; - char *data; + char *data, *filename; GError *error = NULL; data = g_key_file_to_data (config->key_file, &length, NULL); @@ -470,18 +453,50 @@ notmuch_config_save (notmuch_config_t *config) return 1; } - if (! g_file_set_contents (config->filename, data, length, &error)) { - fprintf (stderr, "Error saving configuration to %s: %s\n", - config->filename, error->message); + /* Try not to overwrite symlinks. */ + filename = realpath (config->filename, NULL); + if (! filename) { + if (errno == ENOENT) { + filename = strdup (config->filename); + if (! filename) { + fprintf (stderr, "Out of memory.\n"); + g_free (data); + return 1; + } + } else { + fprintf (stderr, "Error canonicalizing %s: %s\n", config->filename, + strerror (errno)); + g_free (data); + return 1; + } + } + + if (! g_file_set_contents (filename, data, length, &error)) { + if (strcmp (filename, config->filename) != 0) { + fprintf (stderr, "Error saving configuration to %s (-> %s): %s\n", + config->filename, filename, error->message); + } else { + fprintf (stderr, "Error saving configuration to %s: %s\n", + filename, error->message); + } g_error_free (error); + free (filename); g_free (data); return 1; } + free (filename); g_free (data); return 0; } +notmuch_bool_t +notmuch_config_is_new (notmuch_config_t *config) +{ + return config->is_new; +} + + static const char ** _config_get_list (notmuch_config_t *config, const char *section, const char *key, @@ -704,14 +719,8 @@ _item_split (char *item, char **group, char **key) } static int -notmuch_config_command_get (void *ctx, char *item) +notmuch_config_command_get (notmuch_config_t *config, char *item) { - notmuch_config_t *config; - - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - if (strcmp(item, "database.path") == 0) { printf ("%s\n", notmuch_config_get_database_path (config)); } else if (strcmp(item, "user.name") == 0) { @@ -755,25 +764,17 @@ notmuch_config_command_get (void *ctx, char *item) g_strfreev (value); } - notmuch_config_close (config); - return 0; } static int -notmuch_config_command_set (void *ctx, char *item, int argc, char *argv[]) +notmuch_config_command_set (notmuch_config_t *config, char *item, int argc, char *argv[]) { - notmuch_config_t *config; char *group, *key; - int ret; if (_item_split (item, &group, &key)) return 1; - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - /* With only the name of an item, we clear it from the * configuration file. * @@ -794,23 +795,15 @@ notmuch_config_command_set (void *ctx, char *item, int argc, char *argv[]) break; } - ret = notmuch_config_save (config); - notmuch_config_close (config); - - return ret; + return notmuch_config_save (config); } static int -notmuch_config_command_list (void *ctx) +notmuch_config_command_list (notmuch_config_t *config) { - 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; @@ -840,13 +833,11 @@ notmuch_config_command_list (void *ctx) g_strfreev (groups); - notmuch_config_close (config); - return 0; } int -notmuch_config_command (void *ctx, int argc, char *argv[]) +notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]) { argc--; argv++; /* skip subcommand argument */ @@ -861,16 +852,16 @@ notmuch_config_command (void *ctx, int argc, char *argv[]) "one argument.\n"); return 1; } - return notmuch_config_command_get (ctx, argv[1]); + return notmuch_config_command_get (config, argv[1]); } 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); + return notmuch_config_command_set (config, argv[1], argc - 2, argv + 2); } else if (strcmp (argv[0], "list") == 0) { - return notmuch_config_command_list (ctx); + return notmuch_config_command_list (config); } fprintf (stderr, "Unrecognized argument for notmuch config: %s\n", diff --git a/notmuch-count.c b/notmuch-count.c index 2f981282..8772cff8 100644 --- a/notmuch-count.c +++ b/notmuch-count.c @@ -32,17 +32,71 @@ enum { EXCLUDE_FALSE, }; +static int +print_count (notmuch_database_t *notmuch, const char *query_str, + const char **exclude_tags, size_t exclude_tags_length, int output) +{ + notmuch_query_t *query; + size_t i; + + query = notmuch_query_create (notmuch, query_str); + if (query == NULL) { + fprintf (stderr, "Out of memory\n"); + return 1; + } + + for (i = 0; i < exclude_tags_length; i++) + notmuch_query_add_tag_exclude (query, exclude_tags[i]); + + switch (output) { + case OUTPUT_MESSAGES: + printf ("%u\n", notmuch_query_count_messages (query)); + break; + case OUTPUT_THREADS: + printf ("%u\n", notmuch_query_count_threads (query)); + break; + } + + notmuch_query_destroy (query); + + return 0; +} + +static int +count_file (notmuch_database_t *notmuch, FILE *input, const char **exclude_tags, + size_t exclude_tags_length, int output) +{ + char *line = NULL; + ssize_t line_len; + size_t line_size; + int ret = 0; + + while (!ret && (line_len = getline (&line, &line_size, input)) != -1) { + chomp_newline (line); + ret = print_count (notmuch, line, exclude_tags, exclude_tags_length, + output); + } + + if (line) + free (line); + + return ret; +} + int -notmuch_count_command (void *ctx, int argc, char *argv[]) +notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) { - notmuch_config_t *config; notmuch_database_t *notmuch; - notmuch_query_t *query; char *query_str; int opt_index; int output = OUTPUT_MESSAGES; int exclude = EXCLUDE_TRUE; - unsigned int i; + const char **search_exclude_tags = NULL; + size_t search_exclude_tags_length = 0; + notmuch_bool_t batch = FALSE; + FILE *input = stdin; + char *input_file_name = NULL; + int ret; notmuch_opt_desc_t options[] = { { NOTMUCH_OPT_KEYWORD, &output, "output", 'o', @@ -53,6 +107,8 @@ notmuch_count_command (void *ctx, int argc, char *argv[]) (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE }, { "false", EXCLUDE_FALSE }, { 0, 0 } } }, + { NOTMUCH_OPT_BOOLEAN, &batch, "batch", 0, 0 }, + { NOTMUCH_OPT_STRING, &input_file_name, "input", 'i', 0 }, { 0, 0, 0, 0, 0 } }; @@ -62,51 +118,47 @@ notmuch_count_command (void *ctx, int argc, char *argv[]) return 1; } - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) + if (input_file_name) { + batch = TRUE; + input = fopen (input_file_name, "r"); + if (input == NULL) { + fprintf (stderr, "Error opening %s for reading: %s\n", + input_file_name, strerror (errno)); + return 1; + } + } + + if (batch && opt_index != argc) { + fprintf (stderr, "--batch and query string are not compatible\n"); return 1; + } if (notmuch_database_open (notmuch_config_get_database_path (config), NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) return 1; - query_str = query_string_from_args (ctx, argc-opt_index, argv+opt_index); + query_str = query_string_from_args (config, argc-opt_index, argv+opt_index); if (query_str == NULL) { fprintf (stderr, "Out of memory.\n"); return 1; } - if (*query_str == '\0') { - query_str = talloc_strdup (ctx, ""); - } - - query = notmuch_query_create (notmuch, query_str); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); - return 1; - } - 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: - printf ("%u\n", notmuch_query_count_messages (query)); - break; - case OUTPUT_THREADS: - printf ("%u\n", notmuch_query_count_threads (query)); - break; - } + if (batch) + ret = count_file (notmuch, input, search_exclude_tags, + search_exclude_tags_length, output); + else + ret = print_count (notmuch, query_str, search_exclude_tags, + search_exclude_tags_length, output); - notmuch_query_destroy (query); notmuch_database_destroy (notmuch); - return 0; + if (input != stdin) + fclose (input); + + return ret; } diff --git a/notmuch-dump.c b/notmuch-dump.c index a3244e0a..2024e303 100644 --- a/notmuch-dump.c +++ b/notmuch-dump.c @@ -23,9 +23,8 @@ #include "string-util.h" int -notmuch_dump_command (unused (void *ctx), int argc, char *argv[]) +notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]) { - notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_query_t *query; FILE *output = stdout; @@ -34,10 +33,6 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[]) notmuch_tags_t *tags; const char *query_str = ""; - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - if (notmuch_database_open (notmuch_config_get_database_path (config), NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) return 1; diff --git a/notmuch-new.c b/notmuch-new.c index feb9c32f..faa33f1f 100644 --- a/notmuch-new.c +++ b/notmuch-new.c @@ -840,9 +840,8 @@ _remove_directory (void *ctx, } int -notmuch_new_command (void *ctx, int argc, char *argv[]) +notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) { - notmuch_config_t *config; notmuch_database_t *notmuch; add_files_state_t add_files_state; double elapsed; @@ -875,10 +874,6 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) return 1; } - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - add_files_state.new_tags = notmuch_config_get_new_tags (config, &add_files_state.new_tags_length); add_files_state.new_ignore = notmuch_config_get_new_ignore (config, &add_files_state.new_ignore_length); add_files_state.synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config); @@ -890,7 +885,7 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) return ret; } - dot_notmuch_path = talloc_asprintf (ctx, "%s/%s", db_path, ".notmuch"); + dot_notmuch_path = talloc_asprintf (config, "%s/%s", db_path, ".notmuch"); if (stat (dot_notmuch_path, &st)) { int count; @@ -941,9 +936,9 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) add_files_state.removed_messages = add_files_state.renamed_messages = 0; gettimeofday (&add_files_state.tv_start, NULL); - add_files_state.removed_files = _filename_list_create (ctx); - add_files_state.removed_directories = _filename_list_create (ctx); - add_files_state.directory_mtimes = _filename_list_create (ctx); + add_files_state.removed_files = _filename_list_create (config); + add_files_state.removed_directories = _filename_list_create (config); + add_files_state.directory_mtimes = _filename_list_create (config); if (! debugger_is_active () && add_files_state.output_is_a_tty && ! add_files_state.verbose) { @@ -970,7 +965,7 @@ 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++) { - ret = _remove_directory (ctx, notmuch, f->filename, &add_files_state); + ret = _remove_directory (config, notmuch, f->filename, &add_files_state); if (ret) goto DONE; if (do_print_progress) { diff --git a/notmuch-reply.c b/notmuch-reply.c index 22c58ff3..e151f78a 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -702,9 +702,8 @@ enum { }; int -notmuch_reply_command (void *ctx, int argc, char *argv[]) +notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) { - notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_query_t *query; char *query_string; @@ -752,21 +751,17 @@ notmuch_reply_command (void *ctx, int argc, char *argv[]) reply_format_func = notmuch_reply_format_headers_only; } else if (format == FORMAT_JSON) { reply_format_func = notmuch_reply_format_sprinter; - sp = sprinter_json_create (ctx, stdout); + sp = sprinter_json_create (config, stdout); } else if (format == FORMAT_SEXP) { reply_format_func = notmuch_reply_format_sprinter; - sp = sprinter_sexp_create (ctx, stdout); + sp = sprinter_sexp_create (config, stdout); } else { reply_format_func = notmuch_reply_format_default; } notmuch_exit_if_unsupported_format (); - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - - query_string = query_string_from_args (ctx, argc-opt_index, argv+opt_index); + query_string = query_string_from_args (config, argc-opt_index, argv+opt_index); if (query_string == NULL) { fprintf (stderr, "Out of memory\n"); return 1; @@ -787,7 +782,7 @@ notmuch_reply_command (void *ctx, int argc, char *argv[]) return 1; } - if (reply_format_func (ctx, config, query, ¶ms, reply_all, sp) != 0) + if (reply_format_func (config, config, query, ¶ms, reply_all, sp) != 0) return 1; notmuch_crypto_cleanup (¶ms.crypto); diff --git a/notmuch-restore.c b/notmuch-restore.c index cf26a423..1419621c 100644 --- a/notmuch-restore.c +++ b/notmuch-restore.c @@ -120,9 +120,8 @@ parse_sup_line (void *ctx, char *line, } int -notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) +notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) { - notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_bool_t accumulate = FALSE; tag_op_flag_t flags = 0; @@ -139,10 +138,6 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) int opt_index; int input_format = DUMP_FORMAT_AUTO; - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - if (notmuch_database_open (notmuch_config_get_database_path (config), NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) return 1; @@ -187,7 +182,7 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) return 1; } - tag_ops = tag_op_list_create (ctx); + tag_ops = tag_op_list_create (config); if (tag_ops == NULL) { fprintf (stderr, "Out of memory.\n"); return 1; @@ -226,7 +221,7 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) if (line_ctx != NULL) talloc_free (line_ctx); - line_ctx = talloc_new (ctx); + line_ctx = talloc_new (config); if (input_format == DUMP_FORMAT_SUP) { ret = parse_sup_line (line_ctx, line, &query_string, tag_ops); } else { diff --git a/notmuch-search.c b/notmuch-search.c index 0b0a879e..43232011 100644 --- a/notmuch-search.c +++ b/notmuch-search.c @@ -287,12 +287,12 @@ enum { EXCLUDE_TRUE, EXCLUDE_FALSE, EXCLUDE_FLAG, + EXCLUDE_ALL }; int -notmuch_search_command (void *ctx, int argc, char *argv[]) +notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]) { - notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_query_t *query; char *query_str; @@ -335,6 +335,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE }, { "false", EXCLUDE_FALSE }, { "flag", EXCLUDE_FLAG }, + { "all", EXCLUDE_ALL }, { 0, 0 } } }, { NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 }, { NOTMUCH_OPT_INT, &limit, "limit", 'L', 0 }, @@ -349,20 +350,20 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) switch (format_sel) { case NOTMUCH_FORMAT_TEXT: - format = sprinter_text_create (ctx, stdout); + format = sprinter_text_create (config, stdout); break; case NOTMUCH_FORMAT_TEXT0: if (output == OUTPUT_SUMMARY) { fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n"); return 1; } - format = sprinter_text0_create (ctx, stdout); + format = sprinter_text0_create (config, stdout); break; case NOTMUCH_FORMAT_JSON: - format = sprinter_json_create (ctx, stdout); + format = sprinter_json_create (config, stdout); break; case NOTMUCH_FORMAT_SEXP: - format = sprinter_sexp_create (ctx, stdout); + format = sprinter_sexp_create (config, stdout); break; default: /* this should never happen */ @@ -371,10 +372,6 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) notmuch_exit_if_unsupported_format (); - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - if (notmuch_database_open (notmuch_config_get_database_path (config), NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) return 1; @@ -405,7 +402,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) exclude = EXCLUDE_FALSE; } - if (exclude == EXCLUDE_TRUE || exclude == EXCLUDE_FLAG) { + if (exclude != EXCLUDE_FALSE) { const char **search_exclude_tags; size_t search_exclude_tags_length; @@ -414,7 +411,9 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) 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); + notmuch_query_set_omit_excluded (query, NOTMUCH_EXCLUDE_FALSE); + if (exclude == EXCLUDE_ALL) + notmuch_query_set_omit_excluded (query, NOTMUCH_EXCLUDE_ALL); } switch (output) { diff --git a/notmuch-setup.c b/notmuch-setup.c index 94d0aa7b..475248b1 100644 --- a/notmuch-setup.c +++ b/notmuch-setup.c @@ -120,17 +120,15 @@ parse_tag_list (void *ctx, char *response) } int -notmuch_setup_command (unused (void *ctx), +notmuch_setup_command (notmuch_config_t *config, unused (int argc), unused (char *argv[])) { char *response = NULL; size_t response_size = 0; - notmuch_config_t *config; const char **old_other_emails; size_t old_other_emails_len; GPtrArray *other_emails; unsigned int i; - int is_new; const char **new_tags; size_t new_tags_len; const char **search_exclude_tags; @@ -147,9 +145,7 @@ notmuch_setup_command (unused (void *ctx), chomp_newline (response); \ } while (0) - config = notmuch_config_open (ctx, NULL, &is_new); - - if (is_new) + if (notmuch_config_is_new (config)) welcome_message_pre_setup (); prompt ("Your full name [%s]: ", notmuch_config_get_user_name (config)); @@ -168,16 +164,16 @@ notmuch_setup_command (unused (void *ctx), for (i = 0; i < old_other_emails_len; i++) { prompt ("Additional email address [%s]: ", old_other_emails[i]); if (strlen (response)) - g_ptr_array_add (other_emails, talloc_strdup (ctx, response)); + g_ptr_array_add (other_emails, talloc_strdup (config, response)); else - g_ptr_array_add (other_emails, talloc_strdup (ctx, + g_ptr_array_add (other_emails, talloc_strdup (config, old_other_emails[i])); } do { prompt ("Additional email address [Press 'Enter' if none]: "); if (strlen (response)) - g_ptr_array_add (other_emails, talloc_strdup (ctx, response)); + g_ptr_array_add (other_emails, talloc_strdup (config, response)); } while (strlen (response)); if (other_emails->len) notmuch_config_set_user_other_email (config, @@ -191,7 +187,7 @@ notmuch_setup_command (unused (void *ctx), if (strlen (response)) { const char *absolute_path; - absolute_path = make_path_absolute (ctx, response); + absolute_path = make_path_absolute (config, response); notmuch_config_set_database_path (config, absolute_path); } @@ -202,7 +198,7 @@ notmuch_setup_command (unused (void *ctx), prompt ("]: "); if (strlen (response)) { - GPtrArray *tags = parse_tag_list (ctx, response); + GPtrArray *tags = parse_tag_list (config, response); notmuch_config_set_new_tags (config, (const char **) tags->pdata, tags->len); @@ -218,7 +214,7 @@ notmuch_setup_command (unused (void *ctx), prompt ("]: "); if (strlen (response)) { - GPtrArray *tags = parse_tag_list (ctx, response); + GPtrArray *tags = parse_tag_list (config, response); notmuch_config_set_search_exclude_tags (config, (const char **) tags->pdata, @@ -229,7 +225,7 @@ notmuch_setup_command (unused (void *ctx), if (! notmuch_config_save (config)) { - if (is_new) + if (notmuch_config_is_new (config)) welcome_message_post_setup (); return 0; } else { diff --git a/notmuch-show.c b/notmuch-show.c index cbfc2d1c..62178f72 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -335,6 +335,8 @@ show_text_part_content (GMimeObject *part, GMimeStream *stream_out, } #ifdef GMIME_ATLEAST_26 + +/* Get signature status string (GMime 2.6) */ static const char* signature_status_to_string (GMimeSignatureStatus x) { @@ -348,25 +350,8 @@ signature_status_to_string (GMimeSignatureStatus x) } return "unknown"; } -#else -static const char* -signer_status_to_string (GMimeSignerStatus x) -{ - switch (x) { - case GMIME_SIGNER_STATUS_NONE: - return "none"; - case GMIME_SIGNER_STATUS_GOOD: - return "good"; - case GMIME_SIGNER_STATUS_BAD: - return "bad"; - case GMIME_SIGNER_STATUS_ERROR: - return "error"; - } - return "unknown"; -} -#endif -#ifdef GMIME_ATLEAST_26 +/* Signature status sprinter (GMime 2.6) */ static void format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node) { @@ -441,7 +426,27 @@ format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node) sp->end (sp); } -#else + +#else /* GMIME_ATLEAST_26 */ + +/* Get signature status string (GMime 2.4) */ +static const char* +signer_status_to_string (GMimeSignerStatus x) +{ + switch (x) { + case GMIME_SIGNER_STATUS_NONE: + return "none"; + case GMIME_SIGNER_STATUS_GOOD: + return "good"; + case GMIME_SIGNER_STATUS_BAD: + return "bad"; + case GMIME_SIGNER_STATUS_ERROR: + return "error"; + } + return "unknown"; +} + +/* Signature status sprinter (GMime 2.4) */ static void format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node) { @@ -504,7 +509,8 @@ format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node) sp->end (sp); } -#endif + +#endif /* GMIME_ATLEAST_26 */ static notmuch_status_t format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node, @@ -1056,9 +1062,8 @@ enum { }; int -notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[])) +notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) { - notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_query_t *query; char *query_string; @@ -1176,11 +1181,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[])) else params.entire_thread = FALSE; - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; - - query_string = query_string_from_args (ctx, argc-opt_index, argv+opt_index); + query_string = query_string_from_args (config, argc-opt_index, argv+opt_index); if (query_string == NULL) { fprintf (stderr, "Out of memory\n"); return 1; @@ -1202,11 +1203,11 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[])) } /* Create structure printer. */ - sprinter = format->new_sprinter(ctx, stdout); + sprinter = format->new_sprinter(config, stdout); /* If a single message is requested we do not use search_excludes. */ if (params.part >= 0) - ret = do_show_single (ctx, query, format, sprinter, ¶ms); + ret = do_show_single (config, query, format, sprinter, ¶ms); else { /* We always apply set the exclude flag. The * exclude=true|false option controls whether or not we return @@ -1225,7 +1226,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[])) params.omit_excluded = FALSE; } - ret = do_show (ctx, query, format, sprinter, ¶ms); + ret = do_show (config, query, format, sprinter, ¶ms); } notmuch_crypto_cleanup (¶ms.crypto); diff --git a/notmuch-tag.c b/notmuch-tag.c index b54c55dd..9a5d3e71 100644 --- a/notmuch-tag.c +++ b/notmuch-tag.c @@ -97,14 +97,17 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string, notmuch_query_t *query; notmuch_messages_t *messages; notmuch_message_t *message; - int ret = 0; + int ret = NOTMUCH_STATUS_SUCCESS; - /* 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; + if (! (flags & TAG_FLAG_REMOVE_ALL)) { + /* 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; + } + flags |= TAG_FLAG_PRE_OPTIMIZED; } query = notmuch_query_create (notmuch, query_string); @@ -120,7 +123,7 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string, notmuch_messages_valid (messages) && ! interrupted; notmuch_messages_move_to_next (messages)) { message = notmuch_messages_get (messages); - ret = tag_op_list_apply (message, tag_ops, flags | TAG_FLAG_PRE_OPTIMIZED); + ret = tag_op_list_apply (message, tag_ops, flags); notmuch_message_destroy (message); if (ret != NOTMUCH_STATUS_SUCCESS) break; @@ -178,15 +181,15 @@ tag_file (void *ctx, notmuch_database_t *notmuch, tag_op_flag_t flags, } int -notmuch_tag_command (void *ctx, int argc, char *argv[]) +notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) { tag_op_list_t *tag_ops = NULL; char *query_string = NULL; - notmuch_config_t *config; notmuch_database_t *notmuch; struct sigaction action; tag_op_flag_t tag_flags = TAG_FLAG_NONE; notmuch_bool_t batch = FALSE; + notmuch_bool_t remove_all = FALSE; FILE *input = stdin; char *input_file_name = NULL; int opt_index; @@ -202,6 +205,7 @@ notmuch_tag_command (void *ctx, int argc, char *argv[]) notmuch_opt_desc_t options[] = { { NOTMUCH_OPT_BOOLEAN, &batch, "batch", 0, 0 }, { NOTMUCH_OPT_STRING, &input_file_name, "input", 'i', 0 }, + { NOTMUCH_OPT_BOOLEAN, &remove_all, "remove-all", 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -224,21 +228,26 @@ notmuch_tag_command (void *ctx, int argc, char *argv[]) fprintf (stderr, "Can't specify both cmdline and stdin!\n"); return 1; } + if (remove_all) { + fprintf (stderr, "Can't specify both --remove-all and --batch\n"); + return 1; + } } else { - tag_ops = tag_op_list_create (ctx); + tag_ops = tag_op_list_create (config); if (tag_ops == NULL) { fprintf (stderr, "Out of memory.\n"); return 1; } - if (parse_tag_command_line (ctx, argc - opt_index, argv + opt_index, + if (parse_tag_command_line (config, argc - opt_index, argv + opt_index, &query_string, tag_ops)) return 1; - } - config = notmuch_config_open (ctx, NULL, NULL); - if (config == NULL) - return 1; + if (tag_op_list_size (tag_ops) == 0 && ! remove_all) { + fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n"); + return 1; + } + } if (notmuch_database_open (notmuch_config_get_database_path (config), NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) @@ -247,10 +256,13 @@ notmuch_tag_command (void *ctx, int argc, char *argv[]) if (notmuch_config_get_maildir_synchronize_flags (config)) tag_flags |= TAG_FLAG_MAILDIR_SYNC; + if (remove_all) + tag_flags |= TAG_FLAG_REMOVE_ALL; + if (batch) - ret = tag_file (ctx, notmuch, tag_flags, input); + ret = tag_file (config, notmuch, tag_flags, input); else - ret = tag_query (ctx, notmuch, query_string, tag_ops, tag_flags); + ret = tag_query (config, notmuch, query_string, tag_ops, tag_flags); notmuch_database_destroy (notmuch); @@ -22,66 +22,74 @@ #include "notmuch-client.h" -typedef int (*command_function_t) (void *ctx, int argc, char *argv[]); +typedef int (*command_function_t) (notmuch_config_t *config, int argc, char *argv[]); typedef struct command { const char *name; command_function_t function; + notmuch_bool_t create_config; const char *arguments; const char *summary; } command_t; -#define MAX_ALIAS_SUBSTITUTIONS 3 - -typedef struct alias { - const char *name; - const char *substitutions[MAX_ALIAS_SUBSTITUTIONS]; -} alias_t; - -alias_t aliases[] = { - { "part", { "show", "--format=raw"}}, - { "search-tags", {"search", "--output=tags", "*"}} -}; +static int +notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]); static int -notmuch_help_command (void *ctx, int argc, char *argv[]); +notmuch_command (notmuch_config_t *config, int argc, char *argv[]); static command_t commands[] = { - { "setup", notmuch_setup_command, + { NULL, notmuch_command, TRUE, + NULL, + "Notmuch main command." }, + { "setup", notmuch_setup_command, TRUE, NULL, "Interactively setup notmuch for first use." }, - { "new", notmuch_new_command, + { "new", notmuch_new_command, FALSE, "[options...]", "Find and import new messages to the notmuch database." }, - { "search", notmuch_search_command, + { "search", notmuch_search_command, FALSE, "[options...] <search-terms> [...]", "Search for messages matching the given search terms." }, - { "show", notmuch_show_command, + { "show", notmuch_show_command, FALSE, "<search-terms> [...]", "Show all messages matching the search terms." }, - { "count", notmuch_count_command, + { "count", notmuch_count_command, FALSE, "[options...] <search-terms> [...]", "Count messages matching the search terms." }, - { "reply", notmuch_reply_command, + { "reply", notmuch_reply_command, FALSE, "[options...] <search-terms> [...]", "Construct a reply template for a set of messages." }, - { "tag", notmuch_tag_command, + { "tag", notmuch_tag_command, FALSE, "+<tag>|-<tag> [...] [--] <search-terms> [...]" , "Add/remove tags for all messages matching the search terms." }, - { "dump", notmuch_dump_command, + { "dump", notmuch_dump_command, FALSE, "[<filename>] [--] [<search-terms>]", "Create a plain-text dump of the tags for each message." }, - { "restore", notmuch_restore_command, + { "restore", notmuch_restore_command, FALSE, "[--accumulate] [<filename>]", "Restore the tags from the given dump file (see 'dump')." }, - { "config", notmuch_config_command, + { "config", notmuch_config_command, FALSE, "[get|set] <section>.<item> [value ...]", "Get or set settings in the notmuch configuration file." }, - { "help", notmuch_help_command, + { "help", notmuch_help_command, TRUE, /* create but don't save config */ "[<command>]", "This message, or more detailed help for the named command." } }; +static command_t * +find_command (const char *name) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE (commands); i++) + if ((!name && !commands[i].name) || + (name && commands[i].name && strcmp (name, commands[i].name) == 0)) + return &commands[i]; + + return NULL; +} + int notmuch_format_version; static void @@ -101,8 +109,8 @@ usage (FILE *out) for (i = 0; i < ARRAY_SIZE (commands); i++) { command = &commands[i]; - fprintf (out, " %-11s %s\n", - command->name, command->summary); + if (command->name) + fprintf (out, " %-11s %s\n", command->name, command->summary); } fprintf (out, "\n"); @@ -148,10 +156,9 @@ exec_man (const char *page) } static int -notmuch_help_command (void *ctx, int argc, char *argv[]) +notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]) { command_t *command; - unsigned int i; argc--; argv++; /* Ignore "help" */ @@ -170,13 +177,10 @@ notmuch_help_command (void *ctx, int argc, char *argv[]) return 0; } - for (i = 0; i < ARRAY_SIZE (commands); i++) { - command = &commands[i]; - - if (strcmp (argv[0], command->name) == 0) { - char *page = talloc_asprintf (ctx, "notmuch-%s", command->name); - exec_man (page); - } + command = find_command (argv[0]); + if (command) { + char *page = talloc_asprintf (config, "notmuch-%s", command->name); + exec_man (page); } if (strcmp (argv[0], "search-terms") == 0) { @@ -196,29 +200,23 @@ notmuch_help_command (void *ctx, int argc, char *argv[]) * to be more clever about this in the future. */ static int -notmuch (void *ctx) +notmuch_command (notmuch_config_t *config, + unused(int argc), unused(char *argv[])) { - notmuch_config_t *config; - notmuch_bool_t is_new; char *db_path; struct stat st; - config = notmuch_config_open (ctx, NULL, &is_new); - /* If the user has never configured notmuch, then run * notmuch_setup_command which will give a nice welcome message, * and interactively guide the user through the configuration. */ - if (is_new) { - notmuch_config_close (config); - return notmuch_setup_command (ctx, 0, NULL); - } + if (notmuch_config_is_new (config)) + return notmuch_setup_command (config, 0, NULL); /* Notmuch is already configured, but is there a database? */ - db_path = talloc_asprintf (ctx, "%s/%s", + db_path = talloc_asprintf (config, "%s/%s", notmuch_config_get_database_path (config), ".notmuch"); if (stat (db_path, &st)) { - notmuch_config_close (config); if (errno != ENOENT) { fprintf (stderr, "Error looking for notmuch database at %s: %s\n", db_path, strerror (errno)); @@ -250,8 +248,6 @@ notmuch (void *ctx) notmuch_config_get_user_name (config), notmuch_config_get_user_primary_email (config)); - notmuch_config_close (config); - return 0; } @@ -259,10 +255,21 @@ int main (int argc, char *argv[]) { void *local; + char *talloc_report; + const char *command_name = NULL; command_t *command; - alias_t *alias; - unsigned int i, j; - const char **argv_local; + char *config_file_name = NULL; + notmuch_config_t *config; + notmuch_bool_t print_help=FALSE, print_version=FALSE; + int opt_index; + int ret = 0; + + notmuch_opt_desc_t options[] = { + { NOTMUCH_OPT_BOOLEAN, &print_help, "help", 'h', 0 }, + { NOTMUCH_OPT_BOOLEAN, &print_version, "version", 'v', 0 }, + { NOTMUCH_OPT_STRING, &config_file_name, "config", 'c', 0 }, + { 0, 0, 0, 0, 0 } + }; talloc_enable_null_tracking (); @@ -274,82 +281,55 @@ main (int argc, char *argv[]) /* Globally default to the current output format version. */ notmuch_format_version = NOTMUCH_FORMAT_CUR; - if (argc == 1) - return notmuch (local); + opt_index = parse_arguments (argc, argv, options, 1); + if (opt_index < 0) { + /* diagnostics already printed */ + return 1; + } - if (strcmp (argv[1], "--help") == 0) + if (print_help) return notmuch_help_command (NULL, argc - 1, &argv[1]); - if (strcmp (argv[1], "--version") == 0) { + if (print_version) { printf ("notmuch " STRINGIFY(NOTMUCH_VERSION) "\n"); return 0; } - for (i = 0; i < ARRAY_SIZE (aliases); i++) { - alias = &aliases[i]; - - if (strcmp (argv[1], alias->name) == 0) - { - int substitutions; - - argv_local = talloc_size (local, sizeof (char *) * - (argc + MAX_ALIAS_SUBSTITUTIONS - 1)); - if (argv_local == NULL) { - fprintf (stderr, "Out of memory.\n"); - return 1; - } - - /* Copy all substution arguments from the alias. */ - argv_local[0] = argv[0]; - for (j = 0; j < MAX_ALIAS_SUBSTITUTIONS; j++) { - if (alias->substitutions[j] == NULL) - break; - argv_local[j+1] = alias->substitutions[j]; - } - substitutions = j; - - /* And copy all original arguments (skipping the argument - * that matched the alias of course. */ - for (j = 2; j < (unsigned) argc; j++) { - argv_local[substitutions+j-1] = argv[j]; - } - - argc += substitutions - 1; - argv = (char **) argv_local; - } - } + if (opt_index < argc) + command_name = argv[opt_index]; - for (i = 0; i < ARRAY_SIZE (commands); i++) { - command = &commands[i]; - - if (strcmp (argv[1], command->name) == 0) { - int ret; - char *talloc_report; - - ret = (command->function)(local, argc - 1, &argv[1]); - - /* in the future support for this environment variable may - * be supplemented or replaced by command line arguments - * --leak-report and/or --leak-report-full */ + command = find_command (command_name); + if (!command) { + fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", + command_name); + return 1; + } - talloc_report = getenv ("NOTMUCH_TALLOC_REPORT"); + config = notmuch_config_open (local, config_file_name, command->create_config); + if (!config) + return 1; - /* this relies on the previous call to - * talloc_enable_null_tracking */ + ret = (command->function)(config, argc - opt_index, argv + opt_index); - if (talloc_report && strcmp (talloc_report, "") != 0) { - FILE *report = fopen (talloc_report, "w"); - talloc_report_full (NULL, report); - } + notmuch_config_close (config); - return ret; + talloc_report = getenv ("NOTMUCH_TALLOC_REPORT"); + if (talloc_report && strcmp (talloc_report, "") != 0) { + /* this relies on the previous call to + * talloc_enable_null_tracking + */ + + FILE *report = fopen (talloc_report, "w"); + if (report) { + talloc_report_full (NULL, report); + } else { + ret = 1; + fprintf (stderr, "ERROR: unable to write talloc log. "); + perror (talloc_report); } } - fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", - argv[1]); - talloc_free (local); - return 1; + return ret; } diff --git a/performance-test/M00-new b/performance-test/M00-new.sh index 99c3f520..99c3f520 100755 --- a/performance-test/M00-new +++ b/performance-test/M00-new.sh diff --git a/performance-test/M01-dump-restore b/performance-test/M01-dump-restore.sh index be5894a6..be5894a6 100755 --- a/performance-test/M01-dump-restore +++ b/performance-test/M01-dump-restore.sh diff --git a/performance-test/T00-new b/performance-test/T00-new.sh index 553bb8b6..553bb8b6 100755 --- a/performance-test/T00-new +++ b/performance-test/T00-new.sh diff --git a/performance-test/T01-dump-restore b/performance-test/T01-dump-restore.sh index b2ff9400..b2ff9400 100755 --- a/performance-test/T01-dump-restore +++ b/performance-test/T01-dump-restore.sh diff --git a/performance-test/T02-tag b/performance-test/T02-tag.sh index 78cecccc..78cecccc 100755 --- a/performance-test/T02-tag +++ b/performance-test/T02-tag.sh diff --git a/performance-test/notmuch-memory-test b/performance-test/notmuch-memory-test new file mode 100755 index 00000000..3cf28c7f --- /dev/null +++ b/performance-test/notmuch-memory-test @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Run tests +# +# Copyright (c) 2005 Junio C Hamano +# +# Adapted from a Makefile to a shell script by Carl Worth (2010) + +if [ ${BASH_VERSINFO[0]} -lt 4 ]; then + echo "Error: The notmuch test suite requires a bash version >= 4.0" + echo "due to use of associative arrays within the test suite." + echo "Please try again with a newer bash (or help us fix the" + echo "test suite to be more portable). Thanks." + exit 1 +fi + +cd $(dirname "$0") + +for test in M*.sh; do + ./"$test" "$@" +done diff --git a/performance-test/notmuch-time-test b/performance-test/notmuch-time-test index 54a208f7..7113efbf 100755 --- a/performance-test/notmuch-time-test +++ b/performance-test/notmuch-time-test @@ -16,12 +16,6 @@ fi cd $(dirname "$0") -TESTS=" - T00-new - T01-dump-restore - T02-tag -" - -for test in $TESTS; do - ./$test "$@" +for test in T*.sh; do + ./"$test" "$@" done @@ -188,11 +188,6 @@ parse_tag_command_line (void *ctx, int argc, char **argv, tag_op_list_append (tag_ops, argv[i] + 1, is_remove); } - if (tag_op_list_size (tag_ops) == 0) { - fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n"); - return TAG_PARSE_INVALID; - } - *query_str = query_string_from_args (ctx, argc - i, &argv[i]); if (*query_str == NULL || **query_str == '\0') { diff --git a/test/README b/test/README index 81c232dd..d12cff24 100644 --- a/test/README +++ b/test/README @@ -178,11 +178,18 @@ library for your script to use. test_expect_equal_file <file1> <file2> - Identical to test_exepect_equal, except that <file1> and <file2> + Identical to test_expect_equal, except that <file1> and <file2> are files instead of strings. This is a much more robust method to compare formatted textual information, since it also notices whitespace and closing newline differences. + test_expect_equal_json <output> <expected> + + Identical to test_expect_equal, except that the two strings are + treated as JSON and canonicalized before equality testing. This is + useful to abstract away from whitespace differences in the expected + output and that generated by running a notmuch command. + test_debug <script> This takes a single argument, <script>, and evaluates it only @@ -253,3 +260,16 @@ variables which are useful in writing tests: generated script that should be called instead of notmuch to do the counting. The notmuch_counter_value() function prints the current counter value. + +There are also functions which remove various environment-dependent +values from notmuch output; these are useful to ensure that test +results remain consistent across different machines. + + notmuch_search_sanitize + notmuch_show_sanitize + notmuch_show_sanitize_all + notmuch_json_show_sanitize + + All these functions should receive the text to be sanitized as the + input of a pipe, e.g. + output=`notmuch search "..." | notmuch_search_sanitize` diff --git a/test/config b/test/config index cfa1f327..ca4cf330 100755 --- a/test/config +++ b/test/config @@ -57,4 +57,27 @@ maildir.synchronize_flags=true foo.string=this is another string value foo.list=this;is another;list value;" +test_begin_subtest "Top level --config=FILE option" +cp "${NOTMUCH_CONFIG}" alt-config +notmuch --config=alt-config config set user.name "Another Name" +test_expect_equal "$(notmuch --config=alt-config config get user.name)" \ + "Another Name" + +test_begin_subtest "Top level --config=FILE option changed the right file" +test_expect_equal "$(notmuch config get user.name)" \ + "Notmuch Test Suite" + +test_begin_subtest "Read config file through a symlink" +ln -s alt-config alt-config-link +test_expect_equal "$(notmuch --config=alt-config-link config get user.name)" \ + "Another Name" + +test_begin_subtest "Write config file through a symlink" +notmuch --config=alt-config-link config set user.name "Link Name" +test_expect_equal "$(notmuch --config=alt-config-link config get user.name)" \ + "Link Name" + +test_begin_subtest "Writing config file through symlink follows symlink" +test_expect_equal "$(readlink alt-config-link)" "alt-config" + test_done @@ -38,4 +38,50 @@ test_expect_equal \ "0" \ "`notmuch count --output=threads from:cworth and not from:cworth`" +test_begin_subtest "message count is the default for batch count" +notmuch count --batch >OUTPUT <<EOF + +from:cworth +EOF +notmuch count --output=messages >EXPECTED +notmuch count --output=messages from:cworth >>EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "batch message count" +notmuch count --batch --output=messages >OUTPUT <<EOF +from:cworth + +tag:inbox +EOF +notmuch count --output=messages from:cworth >EXPECTED +notmuch count --output=messages >>EXPECTED +notmuch count --output=messages tag:inbox >>EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "batch thread count" +notmuch count --batch --output=threads >OUTPUT <<EOF + +from:cworth +from:cworth and not from:cworth +foo +EOF +notmuch count --output=threads >EXPECTED +notmuch count --output=threads from:cworth >>EXPECTED +notmuch count --output=threads from:cworth and not from:cworth >>EXPECTED +notmuch count --output=threads foo >>EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "batch message count with input file" +cat >INPUT <<EOF +from:cworth + +tag:inbox +EOF +notmuch count --input=INPUT --output=messages >OUTPUT +notmuch count --output=messages from:cworth >EXPECTED +notmuch count --output=messages >>EXPECTED +notmuch count --output=messages tag:inbox >>EXPECTED +test_expect_equal_file EXPECTED OUTPUT + + test_done diff --git a/test/excludes b/test/excludes index 24d653ea..f1ae9ea9 100755 --- a/test/excludes +++ b/test/excludes @@ -166,6 +166,16 @@ ${matching_message_ids[3]} ${matching_message_ids[4]} ${matching_message_ids[5]}" +test_begin_subtest "Search, exclude=all (thread summary)" +output=$(notmuch search --exclude=all tag:test | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/5] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (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=all (messages)" +output=$(notmuch search --exclude=all --output=messages tag:test | notmuch_search_sanitize) +test_expect_equal "$output" "${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) @@ -218,6 +228,18 @@ ${matching_message_ids[1]} ${matching_message_ids[2]} ${matching_message_ids[3]}" +test_begin_subtest "Search, exclude=all: tag in query (thread summary)" +output=$(notmuch search --exclude=all 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=all: tag in query (messages)" +output=$(notmuch search --exclude=all --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 diff --git a/test/notmuch-test b/test/notmuch-test index ca9c3dcb..a0c47d49 100755 --- a/test/notmuch-test +++ b/test/notmuch-test @@ -20,6 +20,7 @@ TESTS=" basic help-test config + setup new count search @@ -64,6 +65,7 @@ TESTS=" hex-escaping parse-time-string search-date + thread-replies " TESTS=${NOTMUCH_TESTS:=$TESTS} diff --git a/test/random-corpus.c b/test/random-corpus.c index 8b7748ef..790193d2 100644 --- a/test/random-corpus.c +++ b/test/random-corpus.c @@ -160,7 +160,7 @@ main (int argc, char **argv) exit (1); } - config = notmuch_config_open (ctx, config_path, NULL); + config = notmuch_config_open (ctx, config_path, FALSE); if (config == NULL) return 1; diff --git a/test/setup b/test/setup new file mode 100755 index 00000000..124ef1c8 --- /dev/null +++ b/test/setup @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +test_description='"notmuch setup"' +. ./test-lib.sh + +test_begin_subtest "Create a new config interactively" +notmuch --config=new-notmuch-config > /dev/null <<EOF +Test Suite +test.suite@example.com +another.suite@example.com + +/path/to/maildir +foo bar +baz +EOF +output=$(notmuch --config=new-notmuch-config config list) +test_expect_equal "$output" "\ +database.path=/path/to/maildir +user.name=Test Suite +user.primary_email=test.suite@example.com +user.other_email=another.suite@example.com; +new.tags=foo;bar; +new.ignore= +search.exclude_tags=baz; +maildir.synchronize_flags=true" + +test_done diff --git a/test/tagging b/test/tagging index 1f5632cb..dc118f33 100755 --- a/test/tagging +++ b/test/tagging @@ -30,6 +30,22 @@ 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 "Remove all" +notmuch tag --remove-all One +notmuch tag --remove-all +tag5 +tag6 +unread Two +output=$(notmuch search \* | notmuch_search_sanitize) +test_expect_equal "$output" "\ +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One () +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (tag5 tag6 unread)" + +test_begin_subtest "Remove all with a no-op" +notmuch tag +inbox +tag1 +unread One +notmuch tag --remove-all +foo +inbox +tag1 -foo +unread Two +output=$(notmuch search \* | notmuch_search_sanitize) +test_expect_equal "$output" "\ +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 unread)" + test_begin_subtest "Special characters in tags" notmuch tag +':" ' \* notmuch tag -':" ' Two diff --git a/test/test-lib.sh b/test/test-lib.sh index 84db7926..ffab1bb5 100644 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -194,22 +194,39 @@ test_fixed=0 test_broken=0 test_success=0 -die () { +_die_common () { code=$? + trap - EXIT + set +ex rm -rf "$TEST_TMPDIR" +} + +die () { + _die_common if test -n "$GIT_EXIT_OK" then exit $code else - echo >&5 "FATAL: Unexpected exit with code $code" + exec >&5 + say_color error '%-6s' FATAL + echo " $test_subtest_name" + echo + echo "Unexpected exit while executing $0. Exit code $code." exit 1 fi } +die_signal () { + _die_common + echo >&5 "FATAL: $0: interrupted by signal" $((code - 128)) + exit $code +} + GIT_EXIT_OK= # Note: TEST_TMPDIR *NOT* exported! TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/notmuch-test-$$.XXXXXX") trap 'die' EXIT +trap 'die_signal' HUP INT TERM test_decode_color () { sed -e 's/.\[1m/<WHITE>/g' \ @@ -498,12 +515,12 @@ test_expect_equal () if ! test_skip "$test_subtest_name" then if [ "$output" = "$expected" ]; then - test_ok_ "$test_subtest_name" + test_ok_ else testname=$this_test.$test_count echo "$expected" > $testname.expected echo "$output" > $testname.output - test_failure_ "$test_subtest_name" "$(diff -u $testname.expected $testname.output)" + test_failure_ "$(diff -u $testname.expected $testname.output)" fi fi } @@ -524,12 +541,12 @@ test_expect_equal_file () if ! test_skip "$test_subtest_name" then if diff -q "$file1" "$file2" >/dev/null ; then - test_ok_ "$test_subtest_name" + test_ok_ else testname=$this_test.$test_count cp "$file1" "$testname.$basename1" cp "$file2" "$testname.$basename2" - test_failure_ "$test_subtest_name" "$(diff -u "$testname.$basename1" "$testname.$basename2")" + test_failure_ "$(diff -u "$testname.$basename1" "$testname.$basename2")" fi fi } @@ -567,9 +584,9 @@ test_emacs_expect_t () { result=$(cat OUTPUT) if [ "$result" = t ] then - test_ok_ "$test_subtest_name" + test_ok_ else - test_failure_ "$test_subtest_name" "${result}" + test_failure_ "${result}" fi else # Restore state after the (non) test. @@ -670,12 +687,12 @@ test_require_external_prereq () { test_ok_ () { if test "$test_subtest_known_broken_" = "t"; then - test_known_broken_ok_ "$@" + test_known_broken_ok_ return fi test_success=$(($test_success + 1)) say_color pass "%-6s" "PASS" - echo " $@" + echo " $test_subtest_name" } test_failure_ () { @@ -684,7 +701,7 @@ test_failure_ () { return fi test_failure=$(($test_failure + 1)) - test_failure_message_ "FAIL" "$@" + test_failure_message_ "FAIL" "$test_subtest_name" "$@" test "$immediate" = "" || { GIT_EXIT_OK=t; exit 1; } return 1 } @@ -701,13 +718,13 @@ test_known_broken_ok_ () { test_reset_state_ test_fixed=$(($test_fixed+1)) say_color pass "%-6s" "FIXED" - echo " $@" + echo " $test_subtest_name" } test_known_broken_failure_ () { test_reset_state_ test_broken=$(($test_broken+1)) - test_failure_message_ "BROKEN" "$@" + test_failure_message_ "BROKEN" "$test_subtest_name" "$@" return 1 } @@ -775,6 +792,7 @@ test_expect_success () { test "$#" = 3 && { prereq=$1; shift; } || prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test-expect-success" + test_subtest_name="$1" test_reset_state_ if ! test_skip "$@" then @@ -784,9 +802,9 @@ test_expect_success () { test_check_missing_external_prereqs_ "$@" || if [ "$run_ret" = 0 -a "$eval_ret" = 0 ] then - test_ok_ "$1" + test_ok_ else - test_failure_ "$@" + test_failure_ "$2" fi fi } @@ -795,6 +813,7 @@ test_expect_code () { test "$#" = 4 && { prereq=$1; shift; } || prereq= test "$#" = 3 || error "bug in the test script: not 3 or 4 parameters to test-expect-code" + test_subtest_name="$2" test_reset_state_ if ! test_skip "$@" then @@ -804,9 +823,9 @@ test_expect_code () { test_check_missing_external_prereqs_ "$@" || if [ "$run_ret" = 0 -a "$eval_ret" = "$1" ] then - test_ok_ "$2" + test_ok_ else - test_failure_ "$@" + test_failure_ "exit code $eval_ret, expected $1" "$3" fi fi } @@ -823,10 +842,10 @@ test_external () { test "$#" = 4 && { prereq=$1; shift; } || prereq= test "$#" = 3 || error >&5 "bug in the test script: not 3 or 4 parameters to test_external" - descr="$1" + test_subtest_name="$1" shift test_reset_state_ - if ! test_skip "$descr" "$@" + if ! test_skip "$test_subtest_name" "$@" then # Announce the script to reduce confusion about the # test output that follows. @@ -837,9 +856,9 @@ test_external () { "$@" 2>&4 if [ "$?" = 0 ] then - test_ok_ "$descr" + test_ok_ else - test_failure_ "$descr" "$@" + test_failure_ "$@" fi fi } @@ -853,11 +872,11 @@ test_external_without_stderr () { stderr="$tmp/git-external-stderr.$$.tmp" test_external "$@" 4> "$stderr" [ -f "$stderr" ] || error "Internal error: $stderr disappeared." - descr="no stderr: $1" + test_subtest_name="no stderr: $1" shift if [ ! -s "$stderr" ]; then rm "$stderr" - test_ok_ "$descr" + test_ok_ else if [ "$verbose" = t ]; then output=`echo; echo Stderr is:; cat "$stderr"` @@ -866,7 +885,7 @@ test_external_without_stderr () { fi # rm first in case test_failure exits. rm "$stderr" - test_failure_ "$descr" "$@" "$output" + test_failure_ "$@" "$output" fi } diff --git a/test/thread-replies b/test/thread-replies new file mode 100755 index 00000000..eeb70d06 --- /dev/null +++ b/test/thread-replies @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2013 Aaron Ecay +# + +test_description='test of proper handling of in-reply-to and references headers' + +# This test makes sure that the thread structure in the notmuch +# database is constructed properly, even in the presence of +# non-RFC-compliant headers' + +. ./test-lib.sh + +test_begin_subtest "Use References when In-Reply-To is broken" +add_message '[id]="foo@one.com"' \ + '[subject]=one' +add_message '[in-reply-to]="mumble"' \ + '[references]="<foo@one.com>"' \ + '[subject]="Re: one"' +output=$(notmuch show --format=json 'subject:one' | notmuch_json_show_sanitize) +expected='[[[{"id": "foo@one.com", + "match": true, + "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, + "date_relative": "2001-01-05", + "tags": ["inbox", "unread"], + "headers": {"Subject": "one", + "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"}, + "body": [{"id": 1, + "content-type": "text/plain", + "content": "This is just a test message (#1)\n"}]}, + [[{"id": "msg-002@notmuch-test-suite", + "match": true, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", + "tags": ["inbox", "unread"], "headers": {"Subject": "Re: one", + "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"}, + "body": [{"id": 1, "content-type": "text/plain", + "content": "This is just a test message (#2)\n"}]}, []]]]]]' +expected=`echo "$expected" | notmuch_json_show_sanitize` +test_expect_equal_json "$output" "$expected" + +test_begin_subtest "Prefer References to In-Reply-To" +add_message '[id]="foo@two.com"' \ + '[subject]=two' +add_message '[in-reply-to]="<bar@baz.com>"' \ + '[references]="<foo@two.com>"' \ + '[subject]="Re: two"' +output=$(notmuch show --format=json 'subject:two' | notmuch_json_show_sanitize) +expected='[[[{"id": "foo@two.com", + "match": true, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"], + "headers": {"Subject": "two", + "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"}, + "body": [{"id": 1, "content-type": "text/plain", + "content": "This is just a test message (#3)\n"}]}, + [[{"id": "msg-004@notmuch-test-suite", "match": true, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"], + "headers": {"Subject": "Re: two", + "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"}, + "body": [{"id": 1, + "content-type": "text/plain", "content": "This is just a test message (#4)\n"}]}, + []]]]]]' +expected=`echo "$expected" | notmuch_json_show_sanitize` +test_expect_equal_json "$output" "$expected" + +test_begin_subtest "Use In-Reply-To when no References" +add_message '[id]="foo@three.com"' \ + '[subject]="three"' +add_message '[in-reply-to]="<foo@three.com>"' \ + '[subject]="Re: three"' +output=$(notmuch show --format=json 'subject:three' | notmuch_json_show_sanitize) +expected='[[[{"id": "foo@three.com", "match": true, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"], + "headers": {"Subject": "three", + "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"}, "body": [{"id": 1, + "content-type": "text/plain", "content": "This is just a test message (#5)\n"}]}, + [[{"id": "msg-006@notmuch-test-suite", "match": true, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"], + "headers": {"Subject": "Re: three", + "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"}, "body": [{"id": 1, + "content-type": "text/plain", "content": "This is just a test message (#6)\n"}]}, + []]]]]]' +expected=`echo "$expected" | notmuch_json_show_sanitize` +test_expect_equal_json "$output" "$expected" + +test_begin_subtest "Use last Reference" +add_message '[id]="foo@four.com"' \ + '[subject]="four"' +add_message '[id]="bar@four.com"' \ + '[subject]="not-four"' +add_message '[in-reply-to]="<baz@four.com>"' \ + '[references]="<baz@four.com> <foo@four.com>"' \ + '[subject]="neither"' +output=$(notmuch show --format=json 'subject:four' | notmuch_json_show_sanitize) +expected='[[[{"id": "foo@four.com", "match": true, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"], + "headers": {"Subject": "four", + "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"}, "body": [{"id": 1, + "content-type": "text/plain", "content": "This is just a test message (#7)\n"}]}, + [[{"id": "msg-009@notmuch-test-suite", "match": false, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"], + "headers": {"Subject": "neither", + "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"}, "body": [{"id": 1, + "content-type": "text/plain", "content": "This is just a test message (#9)\n"}]}, + []]]]], [[{"id": "bar@four.com", "match": true, "excluded": false, + "filename": "YYYYY", + "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"], + "headers": {"Subject": "not-four", + "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"}, "body": [{"id": 1, + "content-type": "text/plain", "content": "This is just a test message (#8)\n"}]}, []]]]' +expected=`echo "$expected" | notmuch_json_show_sanitize` +test_expect_equal_json "$output" "$expected" + + +test_done |