From: David Bremner Date: Sun, 26 May 2013 00:20:39 +0000 (-0300) Subject: Merge branch 'release' X-Git-Tag: 0.16_rc1~116 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=f0dfda5c7797f9db81ce35d270fe0ac406c7fca1;hp=06c70d3d754720f1043a0828f6cee33d2788eaf9 Merge branch 'release' merge back debian-only release into master --- diff --git a/NEWS b/NEWS index 1cea2ecb..8545913f 100644 --- a/NEWS +++ b/NEWS @@ -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 @@ -261,6 +261,9 @@ notmuch_rb_thread_get_total_messages (VALUE self); 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); 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 @@ -91,6 +91,26 @@ notmuch_rb_thread_get_toplevel_messages (VALUE self) return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages); } +/* + * 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 * 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 -# -# Based on "notmuch help" as follows: -# -# Usage: notmuch [args...] -# -# Where and [args...] are as follows: -# -# setup +# Author: Jani Nikula # -# new # -# search [options] [...] +# BUGS: # -# show +# Add space after an --option without parameter (e.g. reply --decrypt) +# on completion. # -# reply -# -# tag +|- [...] [--] [...] -# -# dump [] -# -# restore -# -# help [] -_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/nmbug/nmbug b/contrib/nmbug/nmbug deleted file mode 100755 index f003ef9e..00000000 --- a/contrib/nmbug/nmbug +++ /dev/null @@ -1,648 +0,0 @@ -#!/usr/bin/env perl -# Copyright (c) 2011 David Bremner -# License: same as notmuch - -use strict; -use warnings; -use File::Temp qw(tempdir); -use Pod::Usage; - -no encoding; - -my $NMBGIT = $ENV{NMBGIT} || $ENV{HOME}.'/.nmbug'; - -$NMBGIT .= '/.git' if (-d $NMBGIT.'/.git'); - -my $TAGPREFIX = $ENV{NMBPREFIX} || 'notmuch::'; - -# magic hash for git -my $EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; - -# for encoding - -my $ESCAPE_CHAR = '%'; -my $NO_ESCAPE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'. - '0123456789+-_@=.:,'; -my $MUST_ENCODE = qr{[^\Q$NO_ESCAPE\E]}; -my $ESCAPED_RX = qr{$ESCAPE_CHAR([A-Fa-f0-9]{2})}; - -my %command = ( - archive => \&do_archive, - checkout => \&do_checkout, - commit => \&do_commit, - fetch => \&do_fetch, - help => \&do_help, - log => \&do_log, - merge => \&do_merge, - pull => \&do_pull, - push => \&do_push, - status => \&do_status, - ); - -my $subcommand = shift || usage (); - -if (!exists $command{$subcommand}) { - usage (); -} - -&{$command{$subcommand}}(@ARGV); - -sub git_pipe { - my $envref = (ref $_[0] eq 'HASH') ? shift : {}; - my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef; - my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : undef; - - unshift @_, 'git'; - $envref->{GIT_DIR} ||= $NMBGIT; - spawn ($envref, defined $ioref ? $ioref : (), defined $dir ? $dir : (), @_); -} - -sub git { - my $fh = git_pipe (@_); - my $str = join ('', <$fh>); - unless (close $fh) { - die "'git @_' exited with nonzero value\n"; - } - chomp($str); - return $str; -} - -sub spawn { - my $envref = (ref $_[0] eq 'HASH') ? shift : {}; - my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef; - my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : '-|'; - - die unless @_; - - if (open my $child, $dir) { - return $child; - } - # child - while (my ($key, $value) = each %{$envref}) { - $ENV{$key} = $value; - } - - if (defined $ioref && $dir eq '-|') { - open my $fh, '|-', @_ or die "open |- @_: $!"; - foreach my $line (@{$ioref}) { - print $fh $line, "\n"; - } - exit ! close $fh; - } else { - if ($dir ne '|-') { - open STDIN, '<', '/dev/null' or die "reopening stdin: $!" - } - exec @_; - die "exec @_: $!"; - } -} - - -sub get_tags { - my $prefix = shift; - my @tags; - - my $fh = spawn ('-|', qw/notmuch search --output=tags/, "*") - or die 'error dumping tags'; - - while (<$fh>) { - chomp (); - push @tags, $_ if (m/^$prefix/); - } - unless (close $fh) { - die "'notmuch search --output=tags *' exited with nonzero value\n"; - } - return @tags; -} - - -sub do_archive { - system ('git', "--git-dir=$NMBGIT", 'archive', 'HEAD'); -} - - -sub is_committed { - my $status = shift; - return scalar (@{$status->{added}} ) + scalar (@{$status->{deleted}} ) == 0; -} - - -sub do_commit { - my @args = @_; - - my $status = compute_status (); - - if ( is_committed ($status) ) { - print "Nothing to commit\n"; - return; - } - - my $index = read_tree ('HEAD'); - - update_index ($index, $status); - - my $tree = git ( { GIT_INDEX_FILE => $index }, 'write-tree') - or die 'no output from write-tree'; - - my $parent = git ( 'rev-parse', 'HEAD' ) - or die 'no output from rev-parse'; - - my $commit = git ([ @args ], 'commit-tree', $tree, '-p', $parent) - or die 'commit-tree'; - - git ('update-ref', 'HEAD', $commit); - - unlink $index || die "unlink: $!"; - -} - -sub read_tree { - my $treeish = shift; - my $index = $NMBGIT.'/nmbug.index'; - git ({ GIT_INDEX_FILE => $index }, 'read-tree', '--empty'); - git ({ GIT_INDEX_FILE => $index }, 'read-tree', $treeish); - return $index; -} - -sub update_index { - my $index = shift; - my $status = shift; - - my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index }, - '|-', qw/git update-index --index-info/) - or die 'git update-index'; - - foreach my $pair (@{$status->{deleted}}) { - index_tags_for_msg ($git, $pair->{id}, 'D', $pair->{tag}); - } - - foreach my $pair (@{$status->{added}}) { - index_tags_for_msg ($git, $pair->{id}, 'A', $pair->{tag}); - } - unless (close $git) { - die "'git update-index --index-info' exited with nonzero value\n"; - } - -} - - -sub do_fetch { - my $remote = shift || 'origin'; - - git ('fetch', $remote); -} - - -sub notmuch { - my @args = @_; - system ('notmuch', @args) == 0 or die "notmuch @args failed: $?"; -} - - -sub index_tags { - - my $index = $NMBGIT.'/nmbug.index'; - - my $query = join ' ', map ("tag:$_", get_tags ($TAGPREFIX)); - - my $fh = spawn ('-|', qw/notmuch dump --/, $query) - or die "notmuch dump: $!"; - - git ('read-tree', '--empty'); - my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index }, - '|-', qw/git update-index --index-info/) - or die 'git update-index'; - - while (<$fh>) { - m/ ( [^ ]* ) \s+ \( ([^\)]* ) \) /x || die 'syntax error in dump'; - my ($id,$rest) = ($1,$2); - - #strip prefixes before writing - my @tags = grep { s/^$TAGPREFIX//; } split (' ', $rest); - index_tags_for_msg ($git,$id, 'A', @tags); - } - unless (close $git) { - die "'git update-index --index-info' exited with nonzero value\n"; - } - unless (close $fh) { - die "'notmuch dump -- $query' exited with nonzero value\n"; - } - return $index; -} - -sub index_tags_for_msg { - my $fh = shift; - my $msgid = shift; - my $mode = shift; - - my $hash = $EMPTYBLOB; - my $blobmode = '100644'; - - if ($mode eq 'D') { - $blobmode = '0'; - $hash = '0000000000000000000000000000000000000000'; - } - - foreach my $tag (@_) { - my $tagpath = 'tags/' . encode_for_fs ($msgid) . '/' . encode_for_fs ($tag); - print $fh "$blobmode $hash\t$tagpath\n"; - } -} - - -sub do_checkout { - do_sync (action => 'checkout'); -} - - -sub do_sync { - - my %args = @_; - - my $status = compute_status (); - my ($A_action, $D_action); - - if ($args{action} eq 'checkout') { - $A_action = '-'; - $D_action = '+'; - } else { - $A_action = '+'; - $D_action = '-'; - } - - foreach my $pair (@{$status->{added}}) { - - notmuch ('tag', $A_action.$TAGPREFIX.$pair->{tag}, - 'id:'.$pair->{id}); - } - - foreach my $pair (@{$status->{deleted}}) { - notmuch ('tag', $D_action.$TAGPREFIX.$pair->{tag}, - 'id:'.$pair->{id}); - } - -} - - -sub insist_committed { - - my $status=compute_status(); - if ( !is_committed ($status) ) { - print "Uncommitted changes to $TAGPREFIX* tags in notmuch - -For a summary of changes, run 'nmbug status' -To save your changes, run 'nmbug commit' before merging/pull -To discard your changes, run 'nmbug checkout' -"; - exit (1); - } - -} - - -sub do_pull { - my $remote = shift || 'origin'; - - git ( 'fetch', $remote); - - do_merge (); -} - - -sub do_merge { - insist_committed (); - - my $tempwork = tempdir ('/tmp/nmbug-merge.XXXXXX', CLEANUP => 1); - - git ( { GIT_WORK_TREE => $tempwork }, 'checkout', '-f', 'HEAD'); - - git ( { GIT_WORK_TREE => $tempwork }, 'merge', 'FETCH_HEAD'); - - do_checkout (); -} - - -sub do_log { - # we don't want output trapping here, because we want the pager. - system ( 'git', "--git-dir=$NMBGIT", 'log', '--name-status', @_); -} - - -sub do_push { - my $remote = shift || 'origin'; - - git ('push', $remote); -} - - -sub do_status { - my $status = compute_status (); - - my %output = (); - foreach my $pair (@{$status->{added}}) { - $output{$pair->{id}} ||= {}; - $output{$pair->{id}}{$pair->{tag}} = 'A' - } - - foreach my $pair (@{$status->{deleted}}) { - $output{$pair->{id}} ||= {}; - $output{$pair->{id}}{$pair->{tag}} = 'D' - } - - foreach my $pair (@{$status->{missing}}) { - $output{$pair->{id}} ||= {}; - $output{$pair->{id}}{$pair->{tag}} = 'U' - } - - if (is_unmerged ()) { - foreach my $pair (diff_refs ('A')) { - $output{$pair->{id}} ||= {}; - $output{$pair->{id}}{$pair->{tag}} ||= ' '; - $output{$pair->{id}}{$pair->{tag}} .= 'a'; - } - - foreach my $pair (diff_refs ('D')) { - $output{$pair->{id}} ||= {}; - $output{$pair->{id}}{$pair->{tag}} ||= ' '; - $output{$pair->{id}}{$pair->{tag}} .= 'd'; - } - } - - foreach my $id (sort keys %output) { - foreach my $tag (sort keys %{$output{$id}}) { - printf "%s\t%s\t%s\n", $output{$id}{$tag}, $id, $tag; - } - } -} - - -sub is_unmerged { - - return 0 if (! -f $NMBGIT.'/FETCH_HEAD'); - - my $fetch_head = git ('rev-parse', 'FETCH_HEAD'); - my $base = git ( 'merge-base', 'HEAD', 'FETCH_HEAD'); - - return ($base ne $fetch_head); - -} - -sub compute_status { - my %args = @_; - - my @added; - my @deleted; - my @missing; - - my $index = index_tags (); - - my @maybe_deleted = diff_index ($index, 'D'); - - foreach my $pair (@maybe_deleted) { - - my $id = $pair->{id}; - - my $fh = spawn ('-|', qw/notmuch search --output=files/,"id:$id") - or die "searching for $id"; - if (!<$fh>) { - push @missing, $pair; - } else { - push @deleted, $pair; - } - unless (close $fh) { - die "'notmuch search --output=files id:$id' exited with nonzero value\n"; - } - } - - - @added = diff_index ($index, 'A'); - - unlink $index || die "unlink $index: $!"; - - return { added => [@added], deleted => [@deleted], missing => [@missing] }; -} - - -sub diff_index { - my $index = shift; - my $filter = shift; - - my $fh = git_pipe ({ GIT_INDEX_FILE => $index }, - qw/diff-index --cached/, - "--diff-filter=$filter", qw/--name-only HEAD/ ); - - my @lines = unpack_diff_lines ($fh); - unless (close $fh) { - die "'git diff-index --cached --diff-filter=$filter --name-only HEAD' ", - "exited with nonzero value\n"; - } - return @lines; -} - - -sub diff_refs { - my $filter = shift; - my $ref1 = shift || 'HEAD'; - my $ref2 = shift || 'FETCH_HEAD'; - - my $fh= git_pipe ( 'diff', "--diff-filter=$filter", '--name-only', - $ref1, $ref2); - - my @lines = unpack_diff_lines ($fh); - unless (close $fh) { - die "'git diff --diff-filter=$filter --name-only $ref1 $ref2' ", - "exited with nonzero value\n"; - } - return @lines; -} - - -sub unpack_diff_lines { - my $fh = shift; - - my @found; - while(<$fh>) { - chomp (); - my ($id,$tag) = m|tags/ ([^/]+) / ([^/]+) |x; - - $id = decode_from_fs ($id); - $tag = decode_from_fs ($tag); - - push @found, { id => $id, tag => $tag }; - } - - return @found; -} - - -sub encode_for_fs { - my $str = shift; - - $str =~ s/($MUST_ENCODE)/"$ESCAPE_CHAR".sprintf ("%02x",ord ($1))/ge; - return $str; -} - - -sub decode_from_fs { - my $str = shift; - - $str =~ s/$ESCAPED_RX/ chr (hex ($1))/eg; - - return $str; - -} - - -sub usage { - pod2usage (); - exit (1); -} - - -sub do_help { - pod2usage ( -verbose => 2 ); - exit (0); -} - -__END__ - -=head1 NAME - -nmbug - manage notmuch tags about notmuch - -=head1 SYNOPSIS - -nmbug subcommand [options] - -B for more help - -=head1 OPTIONS - -=head2 Most common commands - -=over 8 - -=item B [message] - -Commit appropriately prefixed tags from the notmuch database to -git. Any extra arguments are used (one per line) as a commit message. - -=item B [remote] - -push local nmbug git state to remote repo - -=item B [remote] - -pull (merge) remote repo changes to notmuch. B is equivalent to -B followed by B. - -=back - -=head2 Other Useful Commands - -=over 8 - -=item B - -Update the notmuch database from git. This is mainly useful to discard -your changes in notmuch relative to git. - -=item B [remote] - -Fetch changes from the remote repo (see merge to bring those changes -into notmuch). - -=item B [subcommand] - -print help [for subcommand] - -=item B [parameters] - -A simple wrapper for git log. After running C, you can -inspect the changes with C - -=item B - -Merge changes from FETCH_HEAD into HEAD, and load the result into -notmuch. - -=item B - -Show pending updates in notmuch or git repo. See below for more -information about the output format. - -=back - -=head2 Less common commands - -=over 8 - -=item B - -Dump a tar archive (using git archive) of the current nmbug tag set. - -=back - -=head1 STATUS FORMAT - -B prints lines of the form - - ng Message-Id tag - -where n is a single character representing notmuch database status - -=over 8 - -=item B - -Tag is present in notmuch database, but not committed to nmbug -(equivalently, tag has been deleted in nmbug repo, e.g. by a pull, but -not restored to notmuch database). - -=item B - -Tag is present in nmbug repo, but not restored to notmuch database -(equivalently, tag has been deleted in notmuch) - -=item B - -Message is unknown (missing from local notmuch database) - -=back - -The second character (if present) represents a difference between remote -git and local. Typically C needs to be run to update this. - -=over 8 - - -=item B - -Tag is present in remote, but not in local git. - - -=item B - -Tag is present in local git, but not in remote git. - - -=back - -=head1 DUMP FORMAT - -Each tag $tag for message with Message-Id $id is written to -an empty file - - tags/encode($id)/encode($tag) - -The encoding preserves alphanumerics, and the characters "+-_@=.:," -(not the quotes). All other octets are replaced with '%' followed by -a two digit hex number. - -=head1 ENVIRONMENT - -B specifies the location of the git repository used by nmbug. -If not specified $HOME/.nmbug is used. - -B specifies the prefix in the notmuch database for tags of -interest to nmbug. If not specified 'notmuch::' is used. diff --git a/contrib/nmbug/nmbug-status b/contrib/nmbug/nmbug-status deleted file mode 100755 index d08ca08d..00000000 --- a/contrib/nmbug/nmbug-status +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/python -# -# Copyright (c) 2011-2012 David Bremner -# License: Same as notmuch -# dependencies -# - python 2.6 for json -# - argparse; either python 2.7, or install separately - -import datetime -import notmuch -import rfc822 -import urllib -import json -import argparse -import os -import subprocess - -# parse command line arguments - -parser = argparse.ArgumentParser() -parser.add_argument('--text', help='output plain text format', - action='store_true') - -parser.add_argument('--config', help='load config from given file') - - -args = parser.parse_args() - -# read config from json file - -if args.config != None: - fp = open(args.config) -else: - nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug')) - - # read only the first line from the pipe - sha1 = subprocess.Popen(['git', '--git-dir', nmbhome, - 'show-ref', '-s', 'config'], - stdout=subprocess.PIPE).stdout.readline() - - sha1 = sha1.rstrip() - - fp = subprocess.Popen(['git', '--git-dir', nmbhome, - 'cat-file', 'blob', sha1+':status-config.json'], - stdout=subprocess.PIPE).stdout - -config = json.load(fp) - -if args.text: - output_format = 'text' -else: - output_format = 'html' - -class Thread: - def __init__(self, last, lines): - self.last = last - self.lines = lines - - def join_utf8_with_newlines(self): - return '\n'.join( (line.encode('utf-8') for line in self.lines) ) - -def output_with_separator(threadlist, sep): - outputs = (thread.join_utf8_with_newlines() for thread in threadlist) - print sep.join(outputs) - -headers = ['date', 'from', 'subject'] - -def print_view(title, query, comment): - - query_string = ' and '.join(query) - q_new = notmuch.Query(db, query_string) - q_new.set_sort(notmuch.Query.SORT.OLDEST_FIRST) - - last_thread_id = '' - threads = {} - threadlist = [] - out = {} - last = None - lines = None - - if output_format == 'html': - print '

%s

' % (title, title) - print comment - print 'The view is generated from the following query:' - print '
' - print query_string - print '
' - print '\n' - - for m in q_new.search_messages(): - - thread_id = m.get_thread_id() - - if thread_id != last_thread_id: - if threads.has_key(thread_id): - last = threads[thread_id].last - lines = threads[thread_id].lines - else: - last = {} - lines = [] - thread = Thread(last, lines) - threads[thread_id] = thread - for h in headers: - last[h] = '' - threadlist.append(thread) - last_thread_id = thread_id - - for header in headers: - val = m.get_header(header) - - if header == 'date': - val = str.join(' ', val.split(None)[1:4]) - val = str(datetime.datetime.strptime(val, '%d %b %Y').date()) - elif header == 'from': - (val, addr) = rfc822.parseaddr(val) - if val == '': - val = addr.split('@')[0] - - if header != 'subject' and last[header] == val: - out[header] = '' - else: - out[header] = val - last[header] = val - - mid = m.get_message_id() - out['id'] = 'id:"%s"' % mid - - if output_format == 'html': - - out['subject'] = '%s' \ - % (urllib.quote(mid), out['subject']) - - lines.append(' ') - lines.append(' ') - else: - lines.append('%(date)-10.10s %(from)-20.20s %(subject)-40.40s\n%(id)72s' % out) - - if output_format == 'html': - output_with_separator(threadlist, - '\n\n') - print '
%s' % out['date']) - lines.append('%s' % out['id']) - lines.append('
%s' % out['from']) - lines.append('%s' % out['subject']) - lines.append('

' - else: - output_with_separator(threadlist, '\n\n') - -# main program - -db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) - -if output_format == 'html': - print ''' - - - - -Notmuch Patches - -''' - print '

Notmuch Patches

' - print 'Generated: %s
' % datetime.datetime.utcnow().date() - print 'For more infomation see nmbug' - - print '

Views

' - print '
    ' - for view in config['views']: - print '
  • %(title)s
  • ' % view - print '
' - -for view in config['views']: - print_view(**view) - -if output_format == 'html': - print '\n' diff --git a/contrib/nmbug/status-config.json b/contrib/nmbug/status-config.json deleted file mode 100644 index 6b4934fa..00000000 --- a/contrib/nmbug/status-config.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "views": [ - { - "comment": "Unresolved bugs (or just need tag updating).", - "query": [ - "tag:notmuch::bug", - "not tag:notmuch::fixed", - "not tag:notmuch::wontfix" - ], - "title": "Bugs" - }, - { - "comment": "These patches are under consideration for pushing.", - "query": [ - "tag:notmuch::patch and not tag:notmuch::pushed", - "not tag:notmuch::obsolete and not tag:notmuch::wip", - "not tag:notmuch::stale and not tag:notmuch::contrib", - "not tag:notmuch::moreinfo", - "not tag:notmuch::python", - "not tag:notmuch::vim", - "not tag:notmuch::wontfix", - "not tag:notmuch::needs-review" - ], - "title": "Maybe Ready (Core and Emacs)" - }, - { - "comment": "These python related patches might be ready to push, or they might just need updated tags.", - "query": [ - "tag:notmuch::patch and not tag:notmuch::pushed", - "not tag:notmuch::obsolete and not tag:notmuch::wip", - "not tag:notmuch::stale and not tag:notmuch::contrib", - "not tag:notmuch::moreinfo", - "not tag:notmuch::wontfix", - " tag:notmuch::python", - "not tag:notmuch::needs-review" - ], - "title": "Maybe Ready (Python)" - }, - { - "comment": "These vim related patches might be ready to push, or they might just need updated tags.", - "query": [ - "tag:notmuch::patch and not tag:notmuch::pushed", - "not tag:notmuch::obsolete and not tag:notmuch::wip", - "not tag:notmuch::stale and not tag:notmuch::contrib", - "not tag:notmuch::moreinfo", - "not tag:notmuch::wontfix", - "tag:notmuch::vim", - "not tag:notmuch::needs-review" - ], - "title": "Maybe Ready (vim)" - }, - { - "comment": "These patches are under review, or waiting for feedback.", - "query": [ - "tag:notmuch::patch", - "not tag:notmuch::pushed", - "not tag:notmuch::obsolete", - "not tag:notmuch::stale", - "not tag:notmuch::wontfix", - "(tag:notmuch::moreinfo or tag:notmuch::needs-review)" - ], - "title": "Review" - } - ] -} 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, or a configuration snippet under F): macro index \ - "unset wait_keynotmuch-mutt -r --prompt search~/.cache/notmuch/mutt/results" \ + "set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key\ + notmuch-mutt -r --prompt search\ + `echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`\ + set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key" \ "notmuch: search mail" + macro index \ - "unset wait_keynotmuch-mutt -r thread~/.cache/notmuch/mutt/resultsset wait_key" \ + "set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key\ + notmuch-mutt -r thread\ + `echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`\ + set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key" \ "notmuch: reconstruct thread" + macro index \ - "unset wait_keynotmuch-mutt tag -- -inbox" \ + "set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key\ + notmuch-mutt tag -- -inbox\ + set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key" \ "notmuch: remove message from inbox" The first macro (activated by ) 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 \ - "unset wait_keynotmuch-mutt -r --prompt search`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`" \ +"set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key\ +notmuch-mutt -r --prompt search\ +`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`\ +set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key" \ "notmuch: search mail" + macro index \ - "unset wait_keynotmuch-mutt -r thread`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`set wait_key" \ +"set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key\ +notmuch-mutt -r thread\ +`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`\ +set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key" \ "notmuch: reconstruct thread" + macro index \ - "unset wait_keynotmuch-mutt tag -- -inbox" \ +"set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key\ +notmuch-mutt tag -- -inbox\ +set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key" \ "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/contrib/notmuch-vim/Makefile b/contrib/notmuch-vim/Makefile new file mode 100644 index 00000000..f17bebfe --- /dev/null +++ b/contrib/notmuch-vim/Makefile @@ -0,0 +1,23 @@ +.PHONY: all help install link symlink + +files = plugin/notmuch.vim \ + $(wildcard syntax/notmuch-*.vim) +prefix = $(HOME)/.vim +destdir = $(prefix)/plugin + +INSTALL = install -D -m644 + +all: help + +help: + @echo "I don't actually build anything, but I will help you install" + @echo "notmuch support for vim." + @echo + @echo " make install - copy plugin scripts and syntax files to ~/.vim" + @echo " make symlink - create symlinks in ~/.vim (useful for development)" + +install: + @for x in $(files); do $(INSTALL) $(CURDIR)/$$x $(prefix)/$$x; done + +link symlink: INSTALL = ln -fs +link symlink: install diff --git a/contrib/notmuch-vim/README b/contrib/notmuch-vim/README new file mode 100644 index 00000000..53f1c4e1 --- /dev/null +++ b/contrib/notmuch-vim/README @@ -0,0 +1,86 @@ +This directory contains a vim script that allows reading notmuch mail +through vim. + +NOTE: this is a work in progress. Patches welcome. + +Dependencies: + notmuch: + Naturally, it expects you have notmuch installed and configured. + + sendmail: + To send mail, notmuch.vim uses sendmail as default. Most modern MTAs + provide a compatibility binary, and so should work well. + + +To install: + make install + + +To run: + vim -c ':NotMuch' + + from vim: + :NotMuch + :NotMuch new to:bart@jukie.net 'subject:this is a test' + + +Buffer types: + [notmuch-folders] + Folder list, or technically a list of saved searches. + + Keybindings: + - show the selected search + m - compose a new message + s - enter search criteria + = - refresh display + + [notmuch-search] + You are presented with the search results when you run :NotMuch. + + Keybindings: + - show the selected thread collapsing unmatched items + - show the entire selected thread + a - archive message (remove inbox tag) + f - filter the current search terms + o - toggle search screen order + m - compose a new message + r - reply to thread + s - enter search criteria + ,s - alter search criteria + t - filter the current search terms with tags + q - return to folder display, or undo filter + + - add tag(s) to selected message + - - remove tag(s) from selected message + = - refresh display + ? - reveal the thread ID of what's under cursor + ^] - search using word under cursor + + [notmuch-show] + This is the display of the message. + + Keybindings: + - mark read, archive, go to next matching message + ^n - next message + ^p - previous message + b - toggle folding of message bodies + c - toggle folding of citations + h - toggle folding of extra header lines + i - toggle folding of signatures + m - compose a new message + r - reply to the message + s - enter search criteria + q - return to search display + ? - reveal the message and thread IDs of what's under cursor + ^] - search using word under cursor + + [notmuch-compose] + When you're writing an email, you're in this mode. + + Insert-mode keybindings: + - go to the next header line + + Normal-mode keybindings: + - go to the next header line + ,s - send this message + ,q - abort this message + diff --git a/contrib/notmuch-vim/notmuch.yaml b/contrib/notmuch-vim/notmuch.yaml new file mode 100644 index 00000000..3d8422c8 --- /dev/null +++ b/contrib/notmuch-vim/notmuch.yaml @@ -0,0 +1,8 @@ +addon: notmuch +description: "notmuch mail user interface" +files: + - plugin/notmuch.vim + - syntax/notmuch-compose.vim + - syntax/notmuch-folders.vim + - syntax/notmuch-search.vim + - syntax/notmuch-show.vim diff --git a/contrib/notmuch-vim/plugin/notmuch.vim b/contrib/notmuch-vim/plugin/notmuch.vim new file mode 100644 index 00000000..8f27fb92 --- /dev/null +++ b/contrib/notmuch-vim/plugin/notmuch.vim @@ -0,0 +1,1432 @@ +" notmuch.vim plugin --- run notmuch within vim +" +" Copyright © Carl Worth +" +" This file is part of Notmuch. +" +" Notmuch is free software: you can redistribute it and/or modify it +" under the terms of the GNU General Public License as published by +" the Free Software Foundation, either version 3 of the License, or +" (at your option) any later version. +" +" Notmuch is distributed in the hope that it will be useful, but +" WITHOUT ANY WARRANTY; without even the implied warranty of +" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +" General Public License for more details. +" +" You should have received a copy of the GNU General Public License +" along with Notmuch. If not, see . +" +" Authors: Bart Trojanowski +" Contributors: Felipe Contreras , +" Peter Hartman +" +" --- configuration defaults {{{1 + +let s:notmuch_defaults = { + \ 'g:notmuch_cmd': 'notmuch' , + \ 'g:notmuch_sendmail': '/usr/sbin/sendmail' , + \ 'g:notmuch_debug': 0 , + \ + \ 'g:notmuch_search_newest_first': 1 , + \ 'g:notmuch_search_from_column_width': 20 , + \ + \ 'g:notmuch_show_fold_signatures': 1 , + \ 'g:notmuch_show_fold_citations': 1 , + \ 'g:notmuch_show_fold_bodies': 0 , + \ 'g:notmuch_show_fold_headers': 1 , + \ + \ 'g:notmuch_show_message_begin_regexp': ' message{' , + \ 'g:notmuch_show_message_end_regexp': ' message}' , + \ 'g:notmuch_show_header_begin_regexp': ' header{' , + \ 'g:notmuch_show_header_end_regexp': ' header}' , + \ 'g:notmuch_show_body_begin_regexp': ' body{' , + \ 'g:notmuch_show_body_end_regexp': ' body}' , + \ 'g:notmuch_show_attachment_begin_regexp': ' attachment{' , + \ 'g:notmuch_show_attachment_end_regexp': ' attachment}' , + \ 'g:notmuch_show_part_begin_regexp': ' part{' , + \ 'g:notmuch_show_part_end_regexp': ' part}' , + \ 'g:notmuch_show_marker_regexp': ' \\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$', + \ + \ 'g:notmuch_show_message_parse_regexp': '\(id:[^ ]*\) depth:\([0-9]*\) match:\([0-9]*\) excluded:\([0-9]*\) filename:\(.*\)$', + \ 'g:notmuch_show_tags_regexp': '(\([^)]*\))$' , + \ + \ 'g:notmuch_show_signature_regexp': '^\(-- \?\|_\+\)$' , + \ 'g:notmuch_show_signature_lines_max': 12 , + \ + \ 'g:notmuch_show_citation_regexp': '^\s*>' , + \ + \ 'g:notmuch_compose_insert_mode_start': 1 , + \ 'g:notmuch_compose_header_help': 1 , + \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose' , + \ } + +" defaults for g:notmuch_initial_search_words +" override with: let g:notmuch_initial_search_words = [ ... ] +let s:notmuch_initial_search_words_defaults = [ + \ 'tag:inbox and tag:unread', + \ ] + +" defaults for g:notmuch_show_headers +" override with: let g:notmuch_show_headers = [ ... ] +let s:notmuch_show_headers_defaults = [ + \ 'Subject', + \ 'To', + \ 'Cc', + \ 'Bcc', + \ 'Date', + \ ] + +" defaults for g:notmuch_folders +" override with: let g:notmuch_folders = [ ... ] +let s:notmuch_folders_defaults = [ + \ [ 'new', 'tag:inbox and tag:unread' ], + \ [ 'inbox', 'tag:inbox' ], + \ [ 'unread', 'tag:unread' ], + \ ] + +" defaults for g:notmuch_signature +" override with: let g:notmuch_signature = [ ... ] +let s:notmuch_signature_defaults = [ + \ '', + \ '-- ', + \ 'email sent from notmuch.vim plugin' + \ ] + +" defaults for g:notmuch_compose_headers +" override with: let g:notmuch_compose_headers = [ ... ] +let s:notmuch_compose_headers_defaults = [ + \ 'From', + \ 'To', + \ 'Cc', + \ 'Bcc', + \ 'Subject' + \ ] + +" --- keyboard mapping definitions {{{1 + +" --- --- bindings for folders mode {{{2 + +let g:notmuch_folders_maps = { + \ 'm': ':call NM_new_mail()', + \ 's': ':call NM_search_prompt()', + \ 'q': ':call NM_kill_this_buffer()', + \ '=': ':call NM_folders_refresh_view()', + \ '': ':call NM_folders_show_search()', + \ } + +" --- --- bindings for search screen {{{2 +let g:notmuch_search_maps = { + \ '': ':call NM_search_show_thread(0)', + \ '': ':call NM_search_show_thread(1)', + \ '': ':call NM_search_expand('''')', + \ 'I': ':call NM_search_mark_read_thread()', + \ 'a': ':call NM_search_archive_thread()', + \ 'A': ':call NM_search_mark_read_then_archive_thread()', + \ 'D': ':call NM_search_delete_thread()', + \ 'f': ':call NM_search_filter()', + \ 'm': ':call NM_new_mail()', + \ 'o': ':call NM_search_toggle_order()', + \ 'r': ':call NM_search_reply_to_thread()', + \ 's': ':call NM_search_prompt()', + \ ',s': ':call NM_search_edit()', + \ 't': ':call NM_search_filter_by_tag()', + \ 'q': ':call NM_kill_this_buffer()', + \ '+': ':call NM_search_add_tags([])', + \ '-': ':call NM_search_remove_tags([])', + \ '=': ':call NM_search_refresh_view()', + \ '?': ':echo NM_search_thread_id() . '' @ '' . join(NM_get_search_words())', + \ } + +" --- --- bindings for show screen {{{2 +let g:notmuch_show_maps = { + \ '': ':call NM_show_previous(1, 0)', + \ '': ':call NM_show_next(1, 0)', + \ '': ':call NM_search_expand('''')', + \ 'q': ':call NM_kill_this_buffer()', + \ 's': ':call NM_search_prompt()', + \ + \ 'b': ':call NM_show_fold_toggle(''b'', ''bdy'', !g:notmuch_show_fold_bodies)', + \ 'c': ':call NM_show_fold_toggle(''c'', ''cit'', !g:notmuch_show_fold_citations)', + \ 'h': ':call NM_show_fold_toggle(''h'', ''hdr'', !g:notmuch_show_fold_headers)', + \ 'i': ':call NM_show_fold_toggle(''i'', ''sig'', !g:notmuch_show_fold_signatures)', + \ + \ 'I': ':call NM_show_mark_read_thread()', + \ 'a': ':call NM_show_archive_thread()', + \ 'A': ':call NM_show_mark_read_then_archive_thread()', + \ 'D': ':call NM_show_delete_thread()', + \ 'd': ':call NM_show_delete_message()', + \ 'N': ':call NM_show_mark_read_then_next_open_message()', + \ 'v': ':call NM_show_view_all_mime_parts()', + \ '+': ':call NM_show_add_tag()', + \ '-': ':call NM_show_remove_tag()', + \ '': ':call NM_show_advance_marking_read_and_archiving()', + \ '\|': ':call NM_show_pipe_message()', + \ + \ '': ':call NM_show_previous_fold()', + \ '': ':call NM_show_next_fold()', + \ '': ':call NM_show_toggle_fold()', + \ + \ 'r': ':call NM_show_reply()', + \ 'm': ':call NM_new_mail()', + \ '?': ':echo NM_show_message_id() . '' @ '' . join(NM_get_search_words())', + \ } + +" --- --- bindings for compose screen {{{2 +let g:notmuch_compose_nmaps = { + \ ',s': ':call NM_compose_send()', + \ ',a': ':call NM_compose_attach()', + \ ',q': ':call NM_kill_this_buffer()', + \ '': ':call NM_compose_next_entry_area()', + \ } +let g:notmuch_compose_imaps = { + \ '': '=NM_compose_next_entry_area()', + \ } + +" --- implement folders screen {{{1 + +function! s:NM_cmd_folders(words) + if len(a:words) + throw 'Not expecting any arguments for folders command.' + endif + let cmd = ['count'] + let disp = [] + let searches = [] + for entry in g:notmuch_folders + let [ name, search ] = entry + let data = s:NM_run(cmd + [search]) + let cnt = matchlist(data, '\(\d\+\)')[1] + call add(disp, printf('%9d %-20s (%s)', cnt, name, search)) + call add(searches, search) + endfor + + call NM_newBuffer('', 'folders', join(disp, "\n")) + let b:nm_searches = searches + let b:nm_timestamp = reltime() + + call NM_cmd_folders_mksyntax() + call NM_set_map('n', g:notmuch_folders_maps) + setlocal cursorline + setlocal nowrap +endfunction + +function! s:NM_cmd_folders_mksyntax() +endfunction + +" --- --- folders screen action functions {{{2 + +function! s:NM_folders_refresh_view() + let lno = line('.') + setlocal bufhidden=delete + call s:NM_cmd_folders([]) + exec printf('norm %dG', lno) +endfunction + +function! s:NM_folders_show_search() + let line = line('.') + let search = b:nm_searches[line-1] + + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call NM_cmd_search([search]) + setlocal bufhidden=delete + let b:nm_prev_bufnr = prev_bufnr +endfunction + + +" --- implement search screen {{{1 + +function! s:NM_cmd_search(words) + let cmd = ['search'] + if g:notmuch_search_newest_first + let cmd = cmd + ['--sort=newest-first'] + else + let cmd = cmd + ['--sort=oldest-first'] + endif + let data = s:NM_run(cmd + a:words) + let lines = split(data, "\n") + let disp = copy(lines) + call map(disp, 's:NM_cmd_search_fmtline(v:val)') + + call NM_newBuffer('', 'search', join(disp, "\n")) + let b:nm_raw_lines = lines + let b:nm_search_words = a:words + + call NM_set_map('n', g:notmuch_search_maps) + setlocal cursorline + setlocal nowrap +endfunction +function! s:NM_cmd_search_fmtline(line) + let m = matchlist(a:line, '^\(thread:\S\+\)\s\(.\{12\}\) \[\(\d\+\)/\d\+\] \([^;]\+\); \%(\[[^\[]\+\] \)*\(.*\) (\([^(]*\))$') + if !len(m) + return 'ERROR PARSING: ' . a:line + endif + let max = g:notmuch_search_from_column_width + let flist = {} + for at in split(m[4], '[|,] ') + let p = split(at, '[@.]') + let flist[p[0]] = 1 + endfor + let from = join(keys(flist), ", ") + return printf("%-12s %3s %-20.20s | %s (%s)", m[2], m[3], from, m[5], m[6]) +endfunction + +" --- --- search screen action functions {{{2 + +function! s:NM_search_show_thread(everything) + let words = [ NM_search_thread_id() ] + if !a:everything && exists('b:nm_search_words') + call extend(words, ['AND', '(']) + call extend(words, b:nm_search_words) + call add(words, ')') + endif + call NM_cmd_show(words) + let b:nm_show_everything = a:everything +endfunction + +function! s:NM_search_prompt() + " TODO: input() can support completion + let text = input('NotMuch Search: ') + if strlen(text) + let tags = split(text) + else + let tags = s:notmuch_initial_search_words_defaults + endif + let prev_bufnr = bufnr('%') + if b:nm_type == 'search' && exists('b:nm_prev_bufnr') + " TODO: we intend to replace the current buffer, + " ... maybe we could just clear it + let prev_bufnr = b:nm_prev_bufnr + setlocal bufhidden=delete + else + setlocal bufhidden=hide + endif + call NM_cmd_search(tags) + setlocal bufhidden=delete + let b:nm_prev_bufnr = prev_bufnr +endfunction + +function! s:NM_search_edit() + " TODO: input() can support completion + let text = input('NotMuch Search: ', join(b:nm_search_words, ' ')) + if strlen(text) + call NM_cmd_search(split(text)) + endif +endfunction + +function! s:NM_search_mark_read_thread() + call NM_tag([], ['-unread']) + norm j +endfunction + +function! s:NM_search_archive_thread() + call NM_tag([], ['-inbox']) + norm j +endfunction + +function! s:NM_search_mark_read_then_archive_thread() + call NM_tag([], ['-unread', '-inbox']) + norm j +endfunction + +function! s:NM_search_delete_thread() + call NM_tag([], ['+delete','-inbox','-unread']) + norm j +endfunction + +function! s:NM_search_filter() + call NM_search_filter_helper('Filter: ', '', '') +endfunction + +function! s:NM_search_filter_by_tag() + call NM_search_filter_helper('Filter Tag(s): ', 'tag:', 'and') +endfunction + +function! s:NM_search_filter_helper(prompt, prefix, joiner) + " TODO: input() can support completion + let text = substitute(input(a:prompt), '\v(^\s*|\s*$|\n)', '', 'g') + if !strlen(text) + return + endif + + let tags = b:nm_search_words + ['AND'] + \ + NM_combine_tags(a:prefix, split(text), a:joiner, '()') + + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call NM_cmd_search(tags) + setlocal bufhidden=delete + let b:nm_prev_bufnr = prev_bufnr +endfunction + +function! s:NM_search_toggle_order() + let g:notmuch_search_newest_first = !g:notmuch_search_newest_first + " FIXME: maybe this would be better done w/o reading re-reading the lines + " reversing the b:nm_raw_lines and the buffer lines would be better + call NM_search_refresh_view() +endfunction + +function! s:NM_search_reply_to_thread() + let cmd = ['reply'] + call add(cmd, NM_search_thread_id()) + call add(cmd, 'AND') + call extend(cmd, NM_get_search_words()) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call NM_newComposeBuffer(lines, 0) +endfunction + +function! s:NM_search_add_tags(tags) + call NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags) +endfunction + +function! s:NM_search_remove_tags(tags) + call NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags) +endfunction + +function! s:NM_search_refresh_view() + let lno = line('.') + let prev_bufnr = b:nm_prev_bufnr + setlocal bufhidden=delete + call NM_cmd_search(b:nm_search_words) + let b:nm_prev_bufnr = prev_bufnr + " FIXME: should find the line of the thread we were on if possible + exec printf('norm %dG', lno) +endfunction + +" --- --- search screen helper functions {{{2 + +function! s:NM_search_thread_id() + if !exists('b:nm_raw_lines') + throw 'Eeek! no b:nm_raw_lines' + endif + let mnum = line('.') - 1 + if len(b:nm_raw_lines) <= mnum + return '' + endif + let info = b:nm_raw_lines[mnum] + let what = split(info, '\s\+')[0] + return what +endfunction + +function! s:NM_search_add_remove_tags(prompt, prefix, intags) + if type(a:intags) != type([]) || len(a:intags) == 0 + " TODO: input() can support completion + let text = input(a:prompt) + if !strlen(text) + return + endif + let tags = split(text, ' ') + else + let tags = a:intags + endif + call map(tags, 'a:prefix . v:val') + call NM_tag([], tags) +endfunction + +" --- implement show screen {{{1 + +function! s:NM_cmd_show(words) + let prev_bufnr = bufnr('%') + let data = s:NM_run(['show', '--entire-thread'] + a:words) + let lines = split(data, "\n") + + let info = s:NM_cmd_show_parse(lines) + + setlocal bufhidden=hide + call NM_newBuffer('', 'show', join(info['disp'], "\n")) + setlocal bufhidden=delete + let b:nm_search_words = a:words + let b:nm_raw_info = info + let b:nm_prev_bufnr = prev_bufnr + + call NM_cmd_show_mkfolds() + call NM_cmd_show_mksyntax() + call NM_set_map('n', g:notmuch_show_maps) + setlocal foldtext=NM_cmd_show_foldtext() + setlocal fillchars= + setlocal foldcolumn=6 + +endfunction + +function! s:NM_show_previous(can_change_thread, find_matching) + let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0 + let info = b:nm_raw_info + let lnum = line('.') + for msg in reverse(copy(info['msgs'])) + if a:find_matching && msg['match'] == '0' + continue + endif + if lnum <= msg['start'] + continue + endif + + exec printf('norm %dGzt', msg['start']) + " TODO: try to fit the message on screen + return + endfor + if !a:can_change_thread + return + endif + call NM_kill_this_buffer() + if line('.') > 1 + norm k + call NM_search_show_thread(everything) + norm G + call NM_show_previous(0, a:find_matching) + else + echo 'No more messages.' + endif +endfunction + +function! s:NM_show_next(can_change_thread, find_matching) + let info = b:nm_raw_info + let lnum = line('.') + for msg in info['msgs'] + if a:find_matching && msg['match'] == '0' + continue + endif + if lnum >= msg['start'] + continue + endif + + exec printf('norm %dGzt', msg['start']) + " TODO: try to fit the message on screen + return + endfor + if a:can_change_thread + call NM_show_next_thread() + endif +endfunction + +function! s:NM_show_next_thread() + let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0 + call NM_kill_this_buffer() + if line('.') != line('$') + norm j + call NM_search_show_thread(everything) + else + echo 'No more messages.' + endif +endfunction + +function! s:NM_show_mark_read_thread() + call NM_tag(b:nm_search_words, ['-unread']) + call NM_show_next_thread() +endfunction + +function! s:NM_show_archive_thread() + call NM_tag(b:nm_search_words, ['-inbox']) + call NM_show_next_thread() +endfunction + +function! s:NM_show_mark_read_then_archive_thread() + call NM_tag(b:nm_search_words, ['-unread', '-inbox']) + call NM_show_next_thread() +endfunction + +function! s:NM_show_delete_thread() + call NM_tag(b:nm_search_words, ['+delete', '-inbox', '-unread']) + call NM_show_next_thread() +endfunction + +function! s:NM_show_delete_message() + let msg = NM_show_get_message_for_line(line('.')) + call NM_tag([msg['id']], ['+delete', '-inbox', '-unread']) +endfunction + +function! s:NM_show_mark_read_then_next_open_message() + echo 'not implemented' +endfunction + +function! s:NM_show_previous_message() + echo 'not implemented' +endfunction + +function! s:NM_show_reply() + let cmd = ['reply'] + call add(cmd, NM_show_message_id()) + call add(cmd, 'AND') + call extend(cmd, NM_get_search_words()) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call NM_newComposeBuffer(lines, 0) +endfunction + +function! s:NM_show_view_all_mime_parts() + echo 'not implemented' +endfunction + +function! s:NM_show_view_raw_message() + echo 'not implemented' +endfunction + +function! s:NM_show_add_tag() + echo 'not implemented' +endfunction + +function! s:NM_show_remove_tag() + echo 'not implemented' +endfunction + +" if entire message is not visible scroll down 1/2 page or less to get to the bottom of message +" otherwise go to next message +" any message that is viewed entirely has inbox and unread tags removed +function! s:NM_show_advance_marking_read_and_archiving() + let advance_tags = ['unread', 'inbox'] + + let vis_top = line('w0') + let vis_bot = line('w$') + + let msg_top = NM_show_get_message_for_line(vis_top) + if !has_key(msg_top,'id') + throw "No top visible message." + endif + + " if the top message is the last message, just expunge the entire thread and move on + if msg_top['end'] == line('$') + let ids = [] + for msg in b:nm_raw_info['msgs'] + if has_key(msg,'match') && msg['match'] != '0' + call add(ids, msg['id']) + endif + endfor + let filter = NM_combine_tags('tag:', advance_tags, 'OR', '()') + \ + ['AND'] + \ + NM_combine_tags('', ids, 'OR', '()') + call map(advance_tags, '"-" . v:val') + call NM_tag(filter, advance_tags) + call NM_show_next(1, 1) + return + endif + + let msg_bot = NM_show_get_message_for_line(vis_bot) + if !has_key(msg_bot,'id') + throw "No bottom visible message." + endif + + " if entire message fits on the screen, read/archive it, move to the next one + if msg_top['id'] != msg_bot['id'] || msg_top['end'] <= vis_bot + exec printf('norm %dG', vis_top) + call NM_show_next(0, 1) + if has_key(msg_top,'match') && msg_top['match'] != '0' + redraw + " do this last to hide the latency + let filter = NM_combine_tags('tag:', advance_tags, 'OR', '()') + \ + ['AND', msg_top['id']] + call map(advance_tags, '"-" . v:val') + call NM_tag(filter, advance_tags) + endif + return + endif + + " entire message does not fit on the screen, scroll down to bottom, max 1/2 screen + let jmp = winheight(winnr()) / 2 + let max = msg_bot['end'] - vis_bot + if jmp > max + let jmp = max + endif + exec printf('norm %dGzt', vis_top + jmp) + return +endfunction + +function! s:NM_show_pipe_message() + echo 'not implemented' +endfunction + +function! s:NM_show_previous_fold() + echo 'not implemented' +endfunction + +function! s:NM_show_next_fold() + echo 'not implemented' +endfunction + +function! s:NM_show_toggle_fold() + echo 'not implemented' +endfunction + + +" --- --- show screen helper functions {{{2 + +function! s:NM_show_get_message_for_line(line) + for msg in b:nm_raw_info['msgs'] + if a:line > msg['end'] + continue + endif + return msg + endfor + return {} +endfunction + +function! s:NM_show_message_id() + if !exists('b:nm_raw_info') + throw 'Eeek! no b:nm_raw_info' + endif + let msg = NM_show_get_message_for_line(line('.')) + if has_key(msg,'id') + return msg['id'] + endif + return '' +endfunction + +function! s:NM_show_fold_toggle(key, type, fold) + let info = b:nm_raw_info + let act = 'open' + if a:fold + let act = 'close' + endif + for fld in info['folds'] + if fld[0] != a:type + continue + endif + "let idx = fld[3] + "let msg = info['msgs'][idx] + "if has_key(msg,'match') && msg['match'] == '0' + " continue + "endif + let cls = foldclosed(fld[1]) + if cls != -1 && cls != fld[1] + continue + endif + exec printf('%dfold%s', fld[1], act) + endfor + exec printf('nnoremap %s :call NM_show_fold_toggle(''%s'', ''%s'', %d)', a:key, a:key, a:type, !a:fold) +endfunction + + +" s:NM_cmd_show_parse returns the following dictionary: +" 'disp': lines to display +" 'msgs': message info dicts { start, end, id, depth, filename, descr, header } +" 'folds': fold info arrays [ type, start, end ] +" 'foldtext': fold text indexed by start line +function! s:NM_cmd_show_parse(inlines) + let info = { 'disp': [], + \ 'msgs': [], + \ 'folds': [], + \ 'foldtext': {} } + let msg = {} + let hdr = {} + + let in_message = 0 + let in_header = 0 + let in_body = 0 + let in_part = '' + + let body_start = -1 + let part_start = -1 + + let mode_type = '' + let mode_start = -1 + + let inlnum = 0 + for line in a:inlines + let inlnum = inlnum + 1 + let foldinfo = [] + + if strlen(in_part) + let part_end = 0 + + if match(line, g:notmuch_show_part_end_regexp) != -1 + let part_end = len(info['disp']) + else + call add(info['disp'], line) + endif + + if in_part == 'text/plain' + if !part_end && mode_type == '' + if match(line, g:notmuch_show_signature_regexp) != -1 + let mode_type = 'sig' + let mode_start = len(info['disp']) + elseif match(line, g:notmuch_show_citation_regexp) != -1 + let mode_type = 'cit' + let mode_start = len(info['disp']) + endif + elseif mode_type == 'cit' + if part_end || match(line, g:notmuch_show_citation_regexp) == -1 + let outlnum = len(info['disp']) + if !part_end + let outlnum = outlnum - 1 + endif + let foldinfo = [ mode_type, mode_start, outlnum, len(info['msgs']), + \ printf('[ %d-line citation. Press "c" to show. ]', 1 + outlnum - mode_start) ] + let mode_type = '' + endif + elseif mode_type == 'sig' + let outlnum = len(info['disp']) + if (outlnum - mode_start) > g:notmuch_show_signature_lines_max + let mode_type = '' + elseif part_end + let foldinfo = [ mode_type, mode_start, outlnum, len(info['msgs']), + \ printf('[ %d-line signature. Press "i" to show. ]', 1 + outlnum - mode_start) ] + let mode_type = '' + endif + endif + endif + + if part_end + " FIXME: this is a hack for handling two folds being added for one line + " we should handle adding a fold in a function + if len(foldinfo) && foldinfo[1] < foldinfo[2] + call add(info['folds'], foldinfo[0:3]) + let info['foldtext'][foldinfo[1]] = foldinfo[4] + endif + + let foldinfo = [ 'text', part_start, part_end, len(info['msgs']), + \ printf('[ %d-line %s. Press "p" to show. ]', part_end - part_start, in_part) ] + let in_part = '' + call add(info['disp'], '') + endif + + elseif in_body + if !has_key(msg,'body_start') + let msg['body_start'] = len(info['disp']) + 1 + endif + if match(line, g:notmuch_show_body_end_regexp) != -1 + let body_end = len(info['disp']) + let foldinfo = [ 'bdy', body_start, body_end, len(info['msgs']), + \ printf('[ BODY %d - %d lines ]', len(info['msgs']), body_end - body_start) ] + + let in_body = 0 + + elseif match(line, g:notmuch_show_part_begin_regexp) != -1 + let m = matchlist(line, 'ID: \(\d\+\), Content-type: \(\S\+\)') + let in_part = 'unknown' + if len(m) + let in_part = m[2] + endif + call add(info['disp'], + \ printf('--- %s ---', in_part)) + " We don't yet handle nested parts, so pop + " multipart/* immediately so text/plain + " sub-parts are parsed properly + if match(in_part, '^multipart/') != -1 + let in_part = '' + else + let part_start = len(info['disp']) + 1 + endif + endif + + elseif in_header + if in_header == 1 + let msg['descr'] = line + call add(info['disp'], line) + let in_header = 2 + let msg['hdr_start'] = len(info['disp']) + 1 + + else + if match(line, g:notmuch_show_header_end_regexp) != -1 + let hdr_start = msg['hdr_start']+1 + let hdr_end = len(info['disp']) + let foldinfo = [ 'hdr', hdr_start, hdr_end, len(info['msgs']), + \ printf('[ %d-line headers. Press "h" to show. ]', hdr_end + 1 - hdr_start) ] + let msg['header'] = hdr + let in_header = 0 + let hdr = {} + else + let m = matchlist(line, '^\(\w\+\):\s*\(.*\)$') + if len(m) + let hdr[m[1]] = m[2] + if match(g:notmuch_show_headers, m[1]) != -1 + call add(info['disp'], line) + endif + endif + endif + endif + + elseif in_message + if match(line, g:notmuch_show_message_end_regexp) != -1 + let msg['end'] = len(info['disp']) + call add(info['disp'], '') + + let foldinfo = [ 'msg', msg['start'], msg['end'], len(info['msgs']), + \ printf('[ MSG %d - %s ]', len(info['msgs']), msg['descr']) ] + + call add(info['msgs'], msg) + let msg = {} + let in_message = 0 + let in_header = 0 + let in_body = 0 + let in_part = '' + + elseif match(line, g:notmuch_show_header_begin_regexp) != -1 + let in_header = 1 + continue + + elseif match(line, g:notmuch_show_body_begin_regexp) != -1 + let body_start = len(info['disp']) + 1 + let in_body = 1 + continue + endif + + else + if match(line, g:notmuch_show_message_begin_regexp) != -1 + let msg['start'] = len(info['disp']) + 1 + + let m = matchlist(line, g:notmuch_show_message_parse_regexp) + if len(m) + let msg['id'] = m[1] + let msg['depth'] = m[2] + let msg['match'] = m[3] + let msg['excluded'] = m[4] + let msg['filename'] = m[5] + endif + + let in_message = 1 + endif + endif + + if len(foldinfo) && foldinfo[1] < foldinfo[2] + call add(info['folds'], foldinfo[0:3]) + let info['foldtext'][foldinfo[1]] = foldinfo[4] + endif + endfor + return info +endfunction + +function! s:NM_cmd_show_mkfolds() + let info = b:nm_raw_info + + for afold in info['folds'] + exec printf('%d,%dfold', afold[1], afold[2]) + let state = 'open' + if (afold[0] == 'sig' && g:notmuch_show_fold_signatures) + \ || (afold[0] == 'cit' && g:notmuch_show_fold_citations) + \ || (afold[0] == 'bdy' && g:notmuch_show_fold_bodies) + \ || (afold[0] == 'hdr' && g:notmuch_show_fold_headers) + let state = 'close' + elseif afold[0] == 'msg' + let idx = afold[3] + let msg = info['msgs'][idx] + if has_key(msg,'match') && msg['match'] == '0' + let state = 'close' + endif + endif + exec printf('%dfold%s', afold[1], state) + endfor +endfunction + +function! s:NM_cmd_show_mksyntax() + let info = b:nm_raw_info + let cnt = 0 + for msg in info['msgs'] + let cnt = cnt + 1 + let start = msg['start'] + let hdr_start = msg['hdr_start'] + let body_start = msg['body_start'] + let end = msg['end'] + exec printf('syntax region nmShowMsg%dDesc start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgDesc', cnt, start, start+1) + exec printf('syntax region nmShowMsg%dHead start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgHead', cnt, hdr_start, body_start) + exec printf('syntax region nmShowMsg%dBody start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgBody', cnt, body_start, end) + endfor +endfunction + +function! NM_cmd_show_foldtext() + let foldtext = b:nm_raw_info['foldtext'] + return foldtext[v:foldstart] +endfunction + + +" --- implement compose screen {{{1 + +function! s:NM_cmd_compose(words, body_lines) + let lines = [] + let start_on_line = 0 + + let hdrs = { } + for word in a:words + let m = matchlist(word, '^\(\w[^:]*\):\s*\(.*\)\s*$') + if !len(m) + throw 'Eeek! bad parameter ''' . string(word) . '''' + endif + let key = substitute(m[1], '\<\w', '\U&', 'g') + if !has_key(hdrs, key) + let hdrs[key] = [] + endif + if strlen(m[2]) + call add(hdrs[key], m[2]) + endif + endfor + + if !has_key(hdrs, 'From') || !len(hdrs['From']) + let me = NM_compose_get_user_email() + let hdrs['From'] = [ me ] + endif + + for key in g:notmuch_compose_headers + let text = has_key(hdrs, key) ? join(hdrs[key], ', ') : '' + call add(lines, key . ': ' . text) + if !start_on_line && !strlen(text) + let start_on_line = len(lines) + endif + endfor + + for [key,val] in items(hdrs) + if match(g:notmuch_compose_headers, key) == -1 + let line = key . ': ' . join(val, ', ') + call add(lines, line) + endif + endfor + + call add(lines, '') + if !start_on_line + let start_on_line = len(lines) + 1 + endif + + if len(a:body_lines) + call extend(lines, a:body_lines) + else + call extend(lines, [ '', '' ]) + endif + + call NM_newComposeBuffer(lines, start_on_line) +endfunction + +function! s:NM_compose_send() + call NM_assert_buffer_type('compose') + let fname = expand('%') + let lnum = 1 + let line = getline(lnum) + let lst_hdr = '' + while match(line, '^$') == -1 + if !exists("hdr_starts") && match(line, '^Notmuch-Help:') == -1 + let hdr_starts = lnum - 1 + endif + let lnum = lnum + 1 + let line = getline(lnum) + endwhile + let body_starts = lnum - 1 + + call append(body_starts, 'Date: ' . strftime('%a, %d %b %Y %H:%M:%S %z')) + exec printf(':0,%dd', hdr_starts) + write + + let line = getline(1) + let m = matchlist(line, '^From:\s*\(.*\)\s*<\(.*\)>$') + if (len(m) >= 2) + let from = m[2] + else + let m = matchlist(line, '^From:\s*\(.*\)$') + let from = m[1] + endif + + let cmdtxt = g:notmuch_sendmail . ' -t -f ' . from . ' < ' . fname + let out = system(cmdtxt) + let err = v:shell_error + if err + undo + write + call NM_newBuffer('new', 'error', + \ "While running...\n" . + \ ' ' . cmdtxt . "\n" . + \ "\n" . + \ "Failed with...\n" . + \ substitute(out, '^', ' ', 'g')) + echohl Error + echo 'Eeek! unable to send mail' + echohl None + return + endif + + if !exists('b:nm_prev_bufnr') + bdelete + else + let prev_bufnr = b:nm_prev_bufnr + bdelete + if prev_bufnr == bufnr('%') + exec printf("buffer %d", prev_bufnr) + endif + endif + call delete(fname) + echo 'Mail sent successfully.' +endfunction + +function! s:NM_compose_attach() + echo 'not implemented' +endfunction + +function! s:NM_compose_next_entry_area() + let lnum = line('.') + let hdr_end = NM_compose_find_line_match(1,'^$',1) + if lnum < hdr_end + let lnum = lnum + 1 + let line = getline(lnum) + if match(line, '^\([^:]\+\):\s*$') == -1 + call cursor(lnum, strlen(line) + 1) + return '' + endif + while match(getline(lnum+1), '^\s') != -1 + let lnum = lnum + 1 + endwhile + call cursor(lnum, strlen(getline(lnum)) + 1) + return '' + + elseif lnum == hdr_end + call cursor(lnum+1, strlen(getline(lnum+1)) + 1) + return '' + endif + if mode() == 'i' + if !getbufvar(bufnr('.'), '&et') + return "\t" + endif + let space = '' + let shiftwidth = a:shiftwidth + let shiftwidth = shiftwidth - ((virtcol('.')-1) % shiftwidth) + " we assume no one has shiftwidth set to more than 40 :) + return ' '[0:shiftwidth] + endif +endfunction + +" --- --- compose screen helper functions {{{2 + +function! s:NM_compose_get_user_email() + " TODO: do this properly (still), i.e., allow for multiple email accounts + let email = substitute(system('notmuch config get user.primary_email'), '\v(^\s*|\s*$|\n)', '', 'g') + return email +endfunction + +function! s:NM_compose_find_line_match(start, pattern, failure) + let lnum = a:start + let lend = line('$') + while lnum < lend + if match(getline(lnum), a:pattern) != -1 + return lnum + endif + let lnum = lnum + 1 + endwhile + return a:failure +endfunction + + +" --- notmuch helper functions {{{1 + +function! s:NM_newBuffer(how, type, content) + if strlen(a:how) + exec a:how + else + enew + endif + setlocal buftype=nofile readonly modifiable scrolloff=0 sidescrolloff=0 + silent put=a:content + keepjumps 0d + setlocal nomodifiable + execute printf('set filetype=notmuch-%s', a:type) + execute printf('set syntax=notmuch-%s', a:type) + let b:nm_type = a:type +endfunction + +function! s:NM_newFileBuffer(fdir, fname, type, lines) + let fdir = expand(a:fdir) + if !isdirectory(fdir) + call mkdir(fdir, 'p') + endif + let file_name = NM_mktemp(fdir, a:fname) + if writefile(a:lines, file_name) + throw 'Eeek! couldn''t write to temporary file ' . file_name + endif + exec printf('edit %s', file_name) + setlocal buftype= noreadonly modifiable scrolloff=0 sidescrolloff=0 + execute printf('set filetype=notmuch-%s', a:type) + execute printf('set syntax=notmuch-%s', a:type) + let b:nm_type = a:type +endfunction + +function! s:NM_newComposeBuffer(lines, start_on_line) + let lines = a:lines + let start_on_line = a:start_on_line + let real_hdr_start = 1 + if g:notmuch_compose_header_help + let help_lines = [ + \ 'Notmuch-Help: Type in your message here; to help you use these bindings:', + \ 'Notmuch-Help: ,a - attach a file', + \ 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)', + \ 'Notmuch-Help: ,q - abort the message', + \ 'Notmuch-Help: - skip through header lines', + \ ] + call extend(lines, help_lines, 0) + let real_hdr_start = len(help_lines) + if start_on_line > 0 + let start_on_line = start_on_line + len(help_lines) + endif + endif + call extend(lines, g:notmuch_signature) + + + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail', + \ 'compose', lines) + setlocal bufhidden=hide + let b:nm_prev_bufnr = prev_bufnr + + call NM_set_map('n', g:notmuch_compose_nmaps) + call NM_set_map('i', g:notmuch_compose_imaps) + + if start_on_line > 0 && start_on_line <= len(lines) + call cursor(start_on_line, strlen(getline(start_on_line)) + 1) + else + call cursor(real_hdr_start, strlen(getline(real_hdr_start)) + 1) + call NM_compose_next_entry_area() + endif + + if g:notmuch_compose_insert_mode_start + startinsert! + endif + echo 'Type your message, use to jump to next header and then body.' +endfunction + +function! s:NM_assert_buffer_type(type) + if !exists('b:nm_type') || b:nm_type != a:type + throw printf('Eeek! expected type %s, but got %s.', a:type, + \ exists(b:nm_type) ? b:nm_type : 'something else') + endif +endfunction + +function! s:NM_mktemp(dir, name) + let time_stamp = strftime('%Y%m%d-%H%M%S') + let file_name = substitute(a:dir,'/*$','/','') . printf(a:name, time_stamp) + " TODO: check if it exists, try again + return file_name +endfunction + +function! s:NM_shell_escape(word) + " TODO: use shellescape() + let word = substitute(a:word, '''', '\\''', 'g') + return '''' . word . '''' +endfunction + +" this function was taken from git.vim, then fixed up +" http://github.com/motemen/git-vim +function! s:NM_shell_split(cmd) + let l:split_cmd = [] + let cmd = a:cmd + let iStart = 0 + while 1 + let t = match(cmd, '\S', iStart) + if t < iStart + break + endif + let iStart = t + + let iSpace = match(cmd, '\v(\s|$)', iStart) + if iSpace < iStart + break + endif + + let iQuote1 = match(cmd, '\(^["'']\|[^\\]\@<=["'']\)', iStart) + if iQuote1 > iSpace || iQuote1 < iStart + let iEnd = iSpace - 1 + let l:split_cmd += [ cmd[iStart : iEnd] ] + else + let q = cmd[iQuote1] + let iQuote2 = match(cmd, '[^\\]\@<=[' . q . ']', iQuote1 + 1) + if iQuote2 < iQuote1 + throw 'No matching ' . q . ' quote' + endif + let iEnd = iQuote2 + let l:split_cmd += [ cmd[iStart+1 : iEnd-1 ] ] + endif + + + let iStart = iEnd + 1 + endwhile + + return l:split_cmd +endfunction + + +function! s:NM_run(args) + let words = a:args + call map(words, 's:NM_shell_escape(v:val)') + let cmd = g:notmuch_cmd . ' ' . join(words) . '< /dev/null' + + if exists('g:notmuch_debug') && g:notmuch_debug + let start = reltime() + let out = system(cmd) + let err = v:shell_error + let delta = reltime(start) + + echo printf('[%s] {%s} %s', reltimestr(delta), string(err), string(cmd)) + else + let out = system(cmd) + let err = v:shell_error + endif + + if err + echohl Error + echo substitute(out, '\n*$', '', '') + echohl None + return '' + else + return out + endif +endfunction + +" --- external mail handling helpers {{{1 + +function! s:NM_new_mail() + call NM_cmd_compose([], []) +endfunction + +" --- tag manipulation helpers {{{1 + +" used to combine an array of words with prefixes and separators +" example: +" NM_combine_tags('tag:', ['one', 'two', 'three'], 'OR', '()') +" -> ['(', 'tag:one', 'OR', 'tag:two', 'OR', 'tag:three', ')'] +function! s:NM_combine_tags(word_prefix, words, separator, brackets) + let res = [] + for word in a:words + if len(res) && strlen(a:separator) + call add(res, a:separator) + endif + call add(res, a:word_prefix . word) + endfor + if len(res) > 1 && strlen(a:brackets) + if strlen(a:brackets) != 2 + throw 'Eeek! brackets arg to NM_combine_tags must be 2 chars' + endif + call insert(res, a:brackets[0]) + call add(res, a:brackets[1]) + endif + return res +endfunction + +" --- other helpers {{{1 + +function! s:NM_get_search_words() + if !exists('b:nm_search_words') + throw 'Eeek! no b:nm_search_words' + endif + return b:nm_search_words +endfunction + +function! s:NM_kill_this_buffer() + if exists('b:nm_prev_bufnr') + let prev_bufnr = b:nm_prev_bufnr + bdelete! + exec printf("buffer %d", prev_bufnr) + else + echo "This is the last buffer; use :q to quit." + endif +endfunction + +function! s:NM_search_expand(arg) + let word = expand(a:arg) + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call NM_cmd_search([word]) + setlocal bufhidden=delete + let b:nm_prev_bufnr = prev_bufnr +endfunction + +function! s:NM_tag(filter, tags) + let filter = len(a:filter) ? a:filter : [NM_search_thread_id()] + if !len(filter) + throw 'Eeek! I couldn''t find the thread id!' + endif + let args = ['tag'] + call extend(args, a:tags) + call add(args, '--') + call extend(args, filter) + " TODO: handle errors + call NM_run(args) +endfunction + +" --- process and set the defaults {{{1 + +function! NM_set_defaults(force) + for [key, dflt] in items(s:notmuch_defaults) + let cmd = '' + if !a:force && exists(key) && type(dflt) == type(eval(key)) + continue + elseif type(dflt) == type(0) + let cmd = printf('let %s = %d', key, dflt) + elseif type(dflt) == type('') + let cmd = printf('let %s = ''%s''', key, dflt) + " FIXME: not sure why this didn't work when dflt is an array + "elseif type(dflt) == type([]) + " let cmd = printf('let %s = %s', key, string(dflt)) + else + echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,%s]', + \ a:force, key, string(dflt)) + continue + endif + exec cmd + endfor +endfunction +call NM_set_defaults(0) + +" for some reason NM_set_defaults() didn't work for arrays... +if !exists('g:notmuch_show_headers') + let g:notmuch_show_headers = s:notmuch_show_headers_defaults +endif +if !exists('g:notmuch_initial_search_words') + let g:notmuch_initial_search_words = s:notmuch_initial_search_words_defaults +endif +if !exists('g:notmuch_folders') + let g:notmuch_folders = s:notmuch_folders_defaults +endif + +if !exists('g:notmuch_signature') + let g:notmuch_signature = s:notmuch_signature_defaults +endif +if !exists('g:notmuch_compose_headers') + let g:notmuch_compose_headers = s:notmuch_compose_headers_defaults +endif + +" --- assign keymaps {{{1 + +function! s:NM_set_map(type, maps) + nmapclear + for [key, code] in items(a:maps) + exec printf('%snoremap %s %s', a:type, key, code) + endfor + " --- this is a hack for development :) + nnoremap ,nmr :runtime! plugin/notmuch.vim +endfunction + +" --- command handler {{{1 + +function! NotMuch(args) + let args = a:args + if !strlen(args) + let args = 'folders' + endif + + let words = NM_shell_split(args) + if words[0] == 'folders' || words[0] == 'f' + let words = words[1:] + call NM_cmd_folders(words) + + elseif words[0] == 'search' || words[0] == 's' + if len(words) > 1 + let words = words[1:] + elseif exists('b:nm_search_words') + let words = b:nm_search_words + else + let words = g:notmuch_initial_search_words + endif + call NM_cmd_search(words) + + elseif words[0] == 'show' + echoe 'show is not yet implemented.' + + elseif words[0] == 'new' || words[0] == 'compose' + let words = words[1:] + call NM_cmd_compose(words, []) + endif +endfunction +function! CompleteNotMuch(arg_lead, cmd_line, cursor_pos) + return [] +endfunction + + +" --- glue {{{1 + +command! -nargs=* -complete=customlist,CompleteNotMuch NotMuch call NotMuch() +cabbrev notmuch =(getcmdtype()==':' && getcmdpos()==1 ? 'NotMuch' : 'notmuch') + +" vim: set ft=vim ts=8 sw=8 et foldmethod=marker : diff --git a/contrib/notmuch-vim/syntax/notmuch-compose.vim b/contrib/notmuch-vim/syntax/notmuch-compose.vim new file mode 100644 index 00000000..19adb756 --- /dev/null +++ b/contrib/notmuch-vim/syntax/notmuch-compose.vim @@ -0,0 +1,7 @@ +runtime! syntax/mail.vim + +syntax region nmComposeHelp contains=nmComposeHelpLine start='^Notmuch-Help:\%1l' end='^\(Notmuch-Help:\)\@!' +syntax match nmComposeHelpLine /Notmuch-Help:/ contained + +highlight link nmComposeHelp Include +highlight link nmComposeHelpLine Error diff --git a/contrib/notmuch-vim/syntax/notmuch-folders.vim b/contrib/notmuch-vim/syntax/notmuch-folders.vim new file mode 100644 index 00000000..9477f86f --- /dev/null +++ b/contrib/notmuch-vim/syntax/notmuch-folders.vim @@ -0,0 +1,12 @@ +" notmuch folders mode syntax file + +syntax region nmFoldersCount start='^' end='\%10v' +syntax region nmFoldersName start='\%11v' end='\%31v' +syntax match nmFoldersSearch /([^()]\+)$/ + +highlight link nmFoldersCount Statement +highlight link nmFoldersName Type +highlight link nmFoldersSearch String + +highlight CursorLine term=reverse cterm=reverse gui=reverse + diff --git a/contrib/notmuch-vim/syntax/notmuch-git-diff.vim b/contrib/notmuch-vim/syntax/notmuch-git-diff.vim new file mode 100644 index 00000000..6f15fdc7 --- /dev/null +++ b/contrib/notmuch-vim/syntax/notmuch-git-diff.vim @@ -0,0 +1,26 @@ +syn match diffRemoved "^-.*" +syn match diffAdded "^+.*" + +syn match diffSeparator "^---$" +syn match diffSubname " @@..*"ms=s+3 contained +syn match diffLine "^@.*" contains=diffSubname + +syn match diffFile "^diff .*" +syn match diffNewFile "^+++ .*" +syn match diffOldFile "^--- .*" + +hi def link diffOldFile diffFile +hi def link diffNewFile diffFile + +hi def link diffFile Type +hi def link diffRemoved Special +hi def link diffAdded Identifier +hi def link diffLine Statement +hi def link diffSubname PreProc + +syntax match gitDiffStatLine /^ .\{-}\zs[+-]\+$/ contains=gitDiffStatAdd,gitDiffStatDelete +syntax match gitDiffStatAdd /+/ contained +syntax match gitDiffStatDelete /-/ contained + +hi def link gitDiffStatAdd diffAdded +hi def link gitDiffStatDelete diffRemoved diff --git a/contrib/notmuch-vim/syntax/notmuch-search.vim b/contrib/notmuch-vim/syntax/notmuch-search.vim new file mode 100644 index 00000000..f458d778 --- /dev/null +++ b/contrib/notmuch-vim/syntax/notmuch-search.vim @@ -0,0 +1,12 @@ +syntax region nmSearch start=/^/ end=/$/ oneline contains=nmSearchDate +syntax match nmSearchDate /^.\{-13}/ contained nextgroup=nmSearchNum +syntax match nmSearchNum /.\{-4}/ contained nextgroup=nmSearchFrom +syntax match nmSearchFrom /.\{-21}/ contained nextgroup=nmSearchSubject +syntax match nmSearchSubject /.\{0,}\(([^()]\+)$\)\@=/ contained nextgroup=nmSearchTags +syntax match nmSearchTags /.\+$/ contained + +highlight link nmSearchDate Statement +highlight link nmSearchNum Type +highlight link nmSearchFrom Include +highlight link nmSearchSubject Normal +highlight link nmSearchTags String diff --git a/contrib/notmuch-vim/syntax/notmuch-show.vim b/contrib/notmuch-vim/syntax/notmuch-show.vim new file mode 100644 index 00000000..c3a98b77 --- /dev/null +++ b/contrib/notmuch-vim/syntax/notmuch-show.vim @@ -0,0 +1,24 @@ +" notmuch show mode syntax file + +syntax cluster nmShowMsgDesc contains=nmShowMsgDescWho,nmShowMsgDescDate,nmShowMsgDescTags +syntax match nmShowMsgDescWho /[^)]\+)/ contained +syntax match nmShowMsgDescDate / ([^)]\+[0-9]) / contained +syntax match nmShowMsgDescTags /([^)]\+)$/ contained + +syntax cluster nmShowMsgHead contains=nmShowMsgHeadKey,nmShowMsgHeadVal +syntax match nmShowMsgHeadKey /^[^:]\+: / contained +syntax match nmShowMsgHeadVal /^\([^:]\+: \)\@<=.*/ contained + +syntax cluster nmShowMsgBody contains=@nmShowMsgBodyMail,@nmShowMsgBodyGit +syntax include @nmShowMsgBodyMail syntax/mail.vim + +silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim + +highlight nmShowMsgDescWho term=reverse cterm=reverse gui=reverse +highlight link nmShowMsgDescDate Type +highlight link nmShowMsgDescTags String + +highlight link nmShowMsgHeadKey Macro +"highlight link nmShowMsgHeadVal NONE + +highlight Folded term=reverse ctermfg=LightGrey ctermbg=Black guifg=LightGray guibg=Black diff --git a/crypto.c b/crypto.c index fbe5aeb6..9736517f 100644 --- a/crypto.c +++ b/crypto.c @@ -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 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. diff --git a/devel/TODO b/devel/TODO index eb757af5..f63385d1 100644 --- a/devel/TODO +++ b/devel/TODO @@ -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 | xargs -0 - -"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 ". 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 " 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 \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 .\n" + unless -d $ARGV[1] . '/manpages'; + +my $ev = 0; +my %fhash; + +open P, '-|', 'find', $ARGV[0], qw/-name *.[0-9] -print/; +while (

) +{ + 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 () { + if (/^\s*$/) { + $emptyline = 1; + next; + } + s/(?<=\S)\s{8,}.*//; # $hl = 'h1' if s/(?<=\S)\s{8,}.*//; + htmlquote $_; + s/[_&]\010&/&/g; + s/((?:_\010[^_])+)/$1<\/u>/g; + s/_\010(.)/$1/g; + s/((?:.\010.)+)/$1<\/b>/g; + s/.\010(.)/$1/g; + + if (/^\S/) { + $pre = 0, push @lines, "\n" if $pre; + s/<\/?b>//g; + chomp; + $_ = "\n<$hl>$_\n"; + $hl = 'h2'; + $emptyline = 0; + } + elsif (/^\s\s\s\S/) { + $pre = 0, push @lines, "\n" if $pre; + s/(?:^\s+)?<\/?b>//g; + chomp; + $_ = "\n

  $_

\n"; + $emptyline = 0; + } + else { + $pre = 1, push @lines, "
\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>\((\d+)\)//) {
+		my $link = maymakelink "$pe-$2-$3";
+		$link = maymakelink "$pe$2-$3" unless $link;
+		if ($link) {
+		    print "$pe-\n";
+		    print "$1$2($3)";
+		}
+		else {
+		    print "$pe-\n";
+		    print "$1$2($3)";
+		}
+	    } else {
+		print "$pe-\n";
+	    }
+	    $pe = '';
+	}
+	s/([^<]+)<\/b>\((\d+)\)/mayconvert($1, $2)/ge;
+	$pe = $1 if s/([^<]+)-<\/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 "$_[0]($_[1])" if exists $fhash{$f};
+    return "$_[0]($_[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 () {
+	if (/^\s*[.]TH\s+\S+\s+(\S+)/) {
+	    $s = $1;
+	    last;
+	}
+    }
+    while () {
+	last if /^\s*[.]SH NAME/
+    }
+    my $line = '';
+    while () {
+	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 = "$n($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  \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 ()
+{
+    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 /^