PV_FILE=bindings/python/notmuch/version.py
# Smash together user's values with our extra values
-FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(CONFIGURE_CFLAGS) $(extra_cflags)
-FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(CONFIGURE_CXXFLAGS) $(extra_cflags) $(extra_cxxflags)
+FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS)
+FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS)
FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lutil -Llib -lnotmuch $(AS_NEEDED_LDFLAGS) $(GMIME_LDFLAGS) $(TALLOC_LDFLAGS)
FINAL_NOTMUCH_LINKER = CC
ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1)
command-line-arguments.c\
debugger.c \
gmime-filter-reply.c \
- gmime-filter-headers.c \
hooks.c \
notmuch.c \
+ notmuch-compact.c \
notmuch-config.c \
notmuch-count.c \
notmuch-dump.c \
+Notmuch 0.17 (2013-12-30)
+=========================
+
+Incompatible change in SHA1 computation
+---------------------------------------
+
+Previously on big endian architectures like sparc and powerpc the
+computation of SHA1 hashes was incorrect. This meant that messages
+with overlong or missing message-ids were given different computed
+message-ids than on more common little endian architectures like i386
+and amd64. If you use notmuch on a big endian architecture, you are
+strongly advised to make a backup of your tags using `notmuch dump`
+before this upgrade. You can locate the affected files using something
+like:
+
+ notmuch dump | \
+ awk '/^notmuch-sha1-[0-9a-f]{40} / \
+ {system("notmuch search --exclude=false --output=files id:" $1)}'
+
+Command-Line Interface
+----------------------
+
+New options to better support handling duplicate messages
+
+ If more than one message file is associated with a message-id,
+ `notmuch search --output=files` will print all of them. A new
+ `--duplicate=N` option can be used to specify which duplicate to
+ print for each message.
+
+ `notmuch count` now supports `--output=files` option to output the
+ number of files associated with matching messages. This may be
+ bigger than the number of matching messages due to duplicates
+ (i.e. multiple files having the same message-id).
+
+Improved `notmuch new` performance for unchanged folders
+
+ `notmuch new` now skips over unchanged folders more efficiently,
+ which can substantially improve the performance of checking for new
+ mail in some situations (like NFS-mounted Maildirs).
+
+`notmuch reply --format=text` RFC 2047-encodes headers
+
+ Previously, this used a mix of standard MIME encoding for the reply
+ body and UTF-8 for the headers. Now, the text format reply template
+ RFC 2047-encodes the headers, making the output a valid RFC 2822
+ message. The JSON/sexp format is unchanged.
+
+`notmuch compact` command
+
+ The new `compact` command exposes Xapian's compaction
+ functionality through a more convenient interface than
+ `xapian-compact`. `notmuch compact` will compact the database to a
+ temporary location, optionally backup the original database, and
+ move the compacted database into place.
+
+Emacs Interface
+---------------
+
+`notmuch-tree` (formerly `notmuch-pick`) has been added to mainline
+
+ `notmuch-tree` is a threaded message view for the emacs
+ interface. Each message is one line in the results and the thread
+ structure is shown using UTF-8 box drawing characters (similar to
+ Mutt's threaded view). It comes between search and show in terms of
+ amount of output and can be useful for viewing both single threads
+ and multiple threads.
+
+ Using `notmuch-tree`
+
+ The main key entries to notmuch tree are
+
+ 'z' enter a query to view using notmuch tree (works in hello,
+ search, show and tree mode itself)
+
+ 'Z' view the current query in tree notmuch tree (works from search
+ and show)
+
+ Once in tree mode, keybindings are mostly in line with the rest of
+ notmuch and are all viewable with '?' as usual.
+
+ Customising `notmuch-tree`
+
+ `notmuch-tree` has several customisation variables. The most
+ significant is the first notmuch-tree-show-out which determines the
+ behaviour when selecting a message (with RET) in tree view. By
+ default tree view uses a split window showing the single message in
+ the bottom pane. However, if this option is set then it views the
+ whole thread in the complete window jumping to the selected message
+ in the thread. In either case command-prefix selects the other option.
+
+Tagging threads in search is now race-free
+
+ Previously, adding or removing a tag from a thread in a search
+ buffer would affect messages that had arrived after the search was
+ performed, resulting in, for example, archiving messages that were
+ never seen. Tagging now affects only the messages that were in the
+ thread when the search was performed.
+
+`notmuch-hello` refreshes when switching to the buffer
+
+ The hello buffer now refreshes whenever you switch to the buffer,
+ regardless of how you get there. You can disable automatic
+ refreshing by customizing `notmuch-hello-auto-refresh`.
+
+Specific mini-buffer prompts for tagging operations
+
+ When entering tags to add or remove, the mini-buffer prompt now
+ indicates what operation will be performed (e.g., "Tag thread", "Tag
+ message", etc).
+
+Built-in help improvements
+
+ Documentation for many commands has been improved, as displayed by
+ `notmuch-help` (usually bound to "?"). The bindings listed by
+ `notmuch-help` also now include descriptions of prefixed commands.
+
+Quote replies as they are displayed in show view
+
+ We now render the parts for reply quoting the same way they are
+ rendered for show. At this time, the notable change is that replies
+ to text/calendar are now pretty instead of raw vcalendar.
+
+Fixed inconsistent use of configured search order
+
+ All ways of interactively invoking search now honor the value of
+ `notmuch-search-oldest-first`.
+
+Common keymap for notmuch-wide bindings
+
+ Several key bindings have been moved from mode-specific keymaps to
+ the single `notmuch-common-keymap`, which is inherited by each
+ notmuch mode. If you've customized your key bindings, you may want
+ to move some of them to the common keymap.
+
+The `notmuch-tag` function now requires a list of tag changes
+
+ For users who have scripted the Emacs interface: the `notmuch-tag`
+ API has changed. Previously, it accepted either a list of tag
+ changes or a space-separated string of tag changes. The latter is
+ no longer supported and the function now returns nothing.
+
+Fixed `notmuch-reply` putting reply in primary selection
+
+ On emacs 24 notmuch-reply used to put the cited text into the
+ primary selection (which could lead to inadvertently pasting this
+ cited text elsewhere). Now the primary-selection is not changed.
+
+Fixed `notmuch-show` invisible part handling
+
+ In some obscure cases part buttons and invisibility had strange
+ interactions: in particular, the default action for some parts gave
+ the wrong action. This has been fixed.
+
+Fixed `notmuch-show` attachment viewers and stderr
+
+ In emacs 24.3+ viewing an attachment could cause spurious text to
+ appear in the show buffer (any stderr or stdout the viewer
+ produced). By default this output is now discarded. For debugging,
+ setting `notmuch-show-attachment-debug` causes notmuch to keep the
+ viewer's stderr and stdout in a separate buffer.
+
+Fixed `notmuch-mua-reply` point placement when signature involved
+
+ By restricting cursor movement to body section for cursor placement
+ after signature is inserted, the cursor cannot "leak" to header
+ section anymore. Now inserted citation content will definitely go to
+ the body part of the message.
+
+Vim Interface
+-------------
+
+ It is now possible to compose new messages in the Vim interface, as
+ opposed reply to existing messages. There is also support for
+ going straight to a search (bypassing the folders view).
+
Notmuch 0.16 (2013-08-03)
=========================
# this file should be kept in sync with ../../../version
-__VERSION__ = '0.16'
+__VERSION__ = '0.17'
notmuch_compat_srcs += $(dir)/strcasestr.c
endif
+ifneq ($(HAVE_STRSEP),1)
+notmuch_compat_srcs += $(dir)/strsep.c
+endif
+
+ifneq ($(HAVE_TIMEGM),1)
+notmuch_compat_srcs += $(dir)/timegm.c
+endif
+
SRCS := $(SRCS) $(notmuch_compat_srcs)
--- /dev/null
+#include <time.h>
+#include <stdio.h>
+
+int main()
+{
+ struct tm tm;
+
+ (void) asctime_r (&tm, NULL);
+
+ return (0);
+}
--- /dev/null
+#include <stdio.h>
+#include <pwd.h>
+
+int main()
+{
+ struct passwd passwd, *ignored;
+
+ (void) getpwuid_r (0, &passwd, NULL, 0, &ignored);
+
+ return (0);
+}
extern "C" {
#endif
+#if !STD_GETPWUID
+#define _POSIX_PTHREAD_SEMANTICS 1
+#endif
+#if !STD_ASCTIME
+#define _POSIX_PTHREAD_SEMANTICS 1
+#endif
+
#if !HAVE_GETLINE
#include <stdio.h>
#include <unistd.h>
char* strcasestr(const char *haystack, const char *needle);
#endif /* !HAVE_STRCASESTR */
+#if !HAVE_STRSEP
+char *strsep(char **stringp, const char *delim);
+#endif /* !HAVE_STRSEP */
+
+#if !HAVE_TIMEGM
+#include <time.h>
+time_t timegm (struct tm *tm);
+#endif /* !HAVE_TIMEGM */
+
/* Silence gcc warnings about unused results. These warnings exist
* for a reason; any use of this needs to be justified. */
#ifdef __GNUC__
--- /dev/null
+#define _GNU_SOURCE
+#include <string.h>
+
+int main()
+{
+ char *found;
+ char **stringp;
+ const char *delim;
+
+ found = strsep(stringp, delim);
+}
--- /dev/null
+#include <time.h>
+#include "compat.h"
+
+int main()
+{
+ return (int) timegm((struct tm *)0);
+}
--- /dev/null
+/* Copyright (C) 1992, 93, 96, 97, 98, 99, 2004 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, write to the Free
+ Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ 02111-1307 USA. */
+
+#include <string.h>
+
+/* Taken from glibc 2.6.1 */
+
+char *strsep (char **stringp, const char *delim)
+{
+ char *begin, *end;
+
+ begin = *stringp;
+ if (begin == NULL)
+ return NULL;
+
+ /* A frequent case is when the delimiter string contains only one
+ character. Here we don't need to call the expensive `strpbrk'
+ function and instead work using `strchr'. */
+ if (delim[0] == '\0' || delim[1] == '\0')
+ {
+ char ch = delim[0];
+
+ if (ch == '\0')
+ end = NULL;
+ else
+ {
+ if (*begin == ch)
+ end = begin;
+ else if (*begin == '\0')
+ end = NULL;
+ else
+ end = strchr (begin + 1, ch);
+ }
+ }
+ else
+ /* Find the end of the token. */
+ end = strpbrk (begin, delim);
+
+ if (end)
+ {
+ /* Terminate the token and set *STRINGP past NUL character. */
+ *end++ = '\0';
+ *stringp = end;
+ }
+ else
+ /* No more delimiters; this is the last token. */
+ *stringp = NULL;
+
+ return begin;
+}
--- /dev/null
+/* timegm.c --- Implementation of replacement timegm function.
+
+ 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, or (at
+ your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ 02110-1301, USA. */
+
+/* Copyright 2013 Blake Jones. */
+
+#include <time.h>
+#include "compat.h"
+
+static int
+leapyear (int year)
+{
+ return ((year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0));
+}
+
+/*
+ * This is a simple implementation of timegm() which does what is needed
+ * by create_output() -- just turns the "struct tm" into a GMT time_t.
+ * It does not normalize any of the fields of the "struct tm", nor does
+ * it set tm_wday or tm_yday.
+ */
+time_t
+timegm (struct tm *tm)
+{
+ int monthlen[2][12] = {
+ { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
+ { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
+ };
+ int year, month, days;
+
+ days = 365 * (tm->tm_year - 70);
+ for (year = 70; year < tm->tm_year; year++) {
+ if (leapyear(1900 + year)) {
+ days++;
+ }
+ }
+ for (month = 0; month < tm->tm_mon; month++) {
+ days += monthlen[leapyear(1900 + year)][month];
+ }
+ days += tm->tm_mday - 1;
+
+ return ((((days * 24) + tm->tm_hour) * 60 + tm->tm_min) * 60 + tm->tm_sec);
+}
__ltrim_colon_completions "${cur}"
}
+_notmuch_compact()
+{
+ local cur prev words cword split
+ _init_completion -s || return
+
+ $split &&
+ case "${prev}" in
+ --backup)
+ _filedir
+ return
+ ;;
+ esac
+
+ ! $split &&
+ case "${cur}" in
+ -*)
+ local options="--backup= --quiet"
+ compopt -o nospace
+ COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+ ;;
+ esac
+}
+
_notmuch_config()
{
local cur prev words cword split
$split &&
case "${prev}" in
--output)
- COMPREPLY=( $( compgen -W "messages threads" -- "${cur}" ) )
+ COMPREPLY=( $( compgen -W "messages threads files" -- "${cur}" ) )
return
;;
--exclude)
COMPREPLY=( $( compgen -W "true false" -- "${cur}" ) )
return
;;
+ --input)
+ _filedir
+ return
+ ;;
esac
! $split &&
case "${cur}" in
-*)
- local options="--output= --exclude="
+ local options="--output= --exclude= --batch --input="
compopt -o nospace
COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
;;
esac
}
+_notmuch_insert()
+{
+ local cur prev words cword split
+ # handle tags with colons and equal signs
+ _init_completion -s -n := || return
+
+ $split &&
+ case "${prev}" in
+ --folder)
+ _filedir
+ return
+ ;;
+ esac
+
+ ! $split &&
+ case "${cur}" in
+ --*)
+ local options="--create-folder --folder="
+ compopt -o nospace
+ COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+ return
+ ;;
+ +*)
+ COMPREPLY=( $(compgen -P "+" -W "`notmuch search --output=tags \*`" -- ${cur##+}) )
+ ;;
+ -*)
+ COMPREPLY=( $(compgen -P "-" -W "`notmuch search --output=tags \*`" -- ${cur##-}) )
+ ;;
+ esac
+ # handle tags with colons
+ __ltrim_colon_completions "${cur}"
+}
+
_notmuch_new()
{
local cur prev words cword split
return
;;
--exclude)
- COMPREPLY=( $( compgen -W "true false flag" -- "${cur}" ) )
+ COMPREPLY=( $( compgen -W "true false flag all" -- "${cur}" ) )
return
;;
esac
! $split &&
case "${cur}" in
-*)
- local options="--format= --output= --sort= --offset= --limit= --exclude="
+ local options="--format= --output= --sort= --offset= --limit= --exclude= --duplicate="
compopt -o nospace
COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
;;
! $split &&
case "${cur}" in
-*)
- local options="--entire-thread= --format= --exclude= --body= --format-version= --part= --verify --decrypt"
+ local options="--entire-thread= --format= --exclude= --body= --format-version= --part= --verify --decrypt --include-html"
compopt -o nospace
COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
;;
{
local cur prev words cword split
# handle tags with colons and equal signs
- _init_completion -n := || return
+ _init_completion -s -n := || return
+
+ $split &&
+ case "${prev}" in
+ --input)
+ _filedir
+ return
+ ;;
+ esac
+ ! $split &&
case "${cur}" in
+ --*)
+ local options="--batch --input= --remove-all"
+ compopt -o nospace
+ COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+ return
+ ;;
+*)
COMPREPLY=( $(compgen -P "+" -W "`notmuch search --output=tags \*`" -- ${cur##+}) )
;;
_notmuch()
{
- local _notmuch_commands="config count dump help new reply restore search setup show tag"
+ local _notmuch_commands="compact config count dump help insert new reply restore search setup show tag"
local arg cur prev words cword split
_init_completion || return
have_xapian=0
for xapian_config in ${XAPIAN_CONFIG}; do
if ${xapian_config} --version > /dev/null 2>&1; then
- printf "Yes (%s).\n" $(${xapian_config} --version | sed -e 's/.* //')
+ xapian_version=$(${xapian_config} --version | sed -e 's/.* //')
+ printf "Yes (%s).\n" ${xapian_version}
have_xapian=1
xapian_cxxflags=$(${xapian_config} --cxxflags)
xapian_ldflags=$(${xapian_config} --libs)
errors=$((errors + 1))
fi
+# Compaction is only supported on Xapian > 1.2.6
+have_xapian_compact=0
+if [ ${have_xapian} = "1" ]; then
+ printf "Checking for Xapian compaction support... "
+ case "${xapian_version}" in
+ 0.*|1.[01].*|1.2.[0-5])
+ printf "No (only available with Xapian > 1.2.6).\n" ;;
+ [1-9]*.[0-9]*.[0-9]*)
+ have_xapian_compact=1
+ printf "Yes.\n" ;;
+ *)
+ printf "Unknown version.\n" ;;
+ esac
+fi
+
printf "Checking for GMime development files... "
have_gmime=0
IFS=';'
EOF
fi
+printf "Checking byte order... "
+cat> _byteorder.c <<EOF
+#include <stdio.h>
+#include <stdint.h>
+uint32_t test = 0x34333231;
+int main() { printf("%.4s\n", (const char*)&test); return 0; }
+EOF
+${CC} ${CFLAGS} _byteorder.c -o _byteorder > /dev/null 2>&1
+util_byte_order=$(./_byteorder)
+echo $util_byte_order
+
+rm -f _byteorder _byteorder.c
+
if [ $errors -gt 0 ]; then
cat <<EOF
fi
rm -f compat/have_strcasestr
+printf "Checking for strsep... "
+if ${CC} -o compat/have_strsep "$srcdir"/compat/have_strsep.c > /dev/null 2>&1
+then
+ printf "Yes.\n"
+ have_strsep="1"
+else
+ printf "No (will use our own instead).\n"
+ have_strsep="0"
+fi
+rm -f compat/have_strsep
+
+printf "Checking for timegm... "
+if ${CC} -o compat/have_timegm "$srcdir"/compat/have_timegm.c > /dev/null 2>&1
+then
+ printf "Yes.\n"
+ have_timegm="1"
+else
+ printf "No (will use our own instead).\n"
+ have_timegm="0"
+fi
+rm -f compat/have_timegm
+
+printf "Checking for standard version of getpwuid_r... "
+if ${CC} -o compat/check_getpwuid "$srcdir"/compat/check_getpwuid.c > /dev/null 2>&1
+then
+ printf "Yes.\n"
+ std_getpwuid=1
+else
+ printf "No (will define _POSIX_PTHREAD_SEMANTICS to get it).\n"
+ std_getpwuid=0
+fi
+rm -f compat/check_getpwuid
+
+printf "Checking for standard version of asctime_r... "
+if ${CC} -o compat/check_asctime "$srcdir"/compat/check_asctime.c > /dev/null 2>&1
+then
+ printf "Yes.\n"
+ std_asctime=1
+else
+ printf "No (will define _POSIX_PTHREAD_SEMANTICS to get it).\n"
+ std_asctime=0
+fi
+rm -f compat/check_asctime
+
printf "int main(void){return 0;}\n" > minimal.c
printf "Checking for rpath support... "
# LIBDIR_IN_LDCONFIG value below is still set correctly.
libdir = ${LIBDIR:=\$(prefix)/lib}
+# byte order within a 32 bit word. 1234 = little, 4321 = big, 0 = guess
+UTIL_BYTE_ORDER = ${util_byte_order}
+
# Whether libdir is in a path configured into ldconfig
LIBDIR_IN_LDCONFIG = ${libdir_in_ldconfig}
# build its own version)
HAVE_STRCASESTR = ${have_strcasestr}
+# Whether the strsep function is available (if not, then notmuch will
+# build its own version)
+HAVE_STRSEP = ${have_strsep}
+
+# Whether the Xapian version in use supports compaction
+HAVE_XAPIAN_COMPACT = ${have_xapian_compact}
+
+# Whether the getpwuid_r function is standards-compliant
+# (if not, then notmuch will #define _POSIX_PTHREAD_SEMANTICS
+# to enable the standards-compliant version -- needed for Solaris)
+STD_GETPWUID = ${std_getpwuid}
+
+# Whether the asctime_r function is standards-compliant
+# (if not, then notmuch will #define _POSIX_PTHREAD_SEMANTICS
+# to enable the standards-compliant version -- needed for Solaris)
+STD_ASCTIME = ${std_asctime}
+
# Supported platforms (so far) are: LINUX, MACOSX, SOLARIS, FREEBSD, OPENBSD
PLATFORM = ${platform}
# Combined flags for compiling and linking against all of the above
CONFIGURE_CFLAGS = -DHAVE_GETLINE=\$(HAVE_GETLINE) \$(GMIME_CFLAGS) \\
\$(TALLOC_CFLAGS) -DHAVE_VALGRIND=\$(HAVE_VALGRIND) \\
- \$(VALGRIND_CFLAGS) -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR)
+ \$(VALGRIND_CFLAGS) \\
+ -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR) \\
+ -DHAVE_STRSEP=\$(HAVE_STRSEP) \\
+ -DSTD_GETPWUID=\$(STD_GETPWUID) \\
+ -DSTD_ASCTIME=\$(STD_ASCTIME) \\
+ -DHAVE_XAPIAN_COMPACT=\$(HAVE_XAPIAN_COMPACT) \\
+ -DUTIL_BYTE_ORDER=\$(UTIL_BYTE_ORDER)
+
CONFIGURE_CXXFLAGS = -DHAVE_GETLINE=\$(HAVE_GETLINE) \$(GMIME_CFLAGS) \\
\$(TALLOC_CFLAGS) -DHAVE_VALGRIND=\$(HAVE_VALGRIND) \\
\$(VALGRIND_CFLAGS) \$(XAPIAN_CXXFLAGS) \\
- -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR)
+ -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR) \\
+ -DHAVE_STRSEP=\$(HAVE_STRSEP) \\
+ -DSTD_GETPWUID=\$(STD_GETPWUID) \\
+ -DSTD_ASCTIME=\$(STD_ASCTIME) \\
+ -DHAVE_XAPIAN_COMPACT=\$(HAVE_XAPIAN_COMPACT) \\
+ -DUTIL_BYTE_ORDER=\$(UTIL_BYTE_ORDER)
+
CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(XAPIAN_LDFLAGS)
EOF
(Debian package: libstring-shellquote-perl)
- Term::ReadLine <http://search.cpan.org/~hayashi/Term-ReadLine-Gnu/>
(Debian package: libterm-readline-gnu-perl)
-- File::Which <http://search.cpan.org/dist/File-Which/>
- (Debian package: libfile-which-perl)
-
-The --remove-dups option will use fdupes <https://code.google.com/p/fdupes/>
-if it is installed. Version fdupes-1.50-PR2 or higher is required.
To *build* notmuch-mutt documentation you will need:
use Pod::Usage;
use String::ShellQuote;
use Term::ReadLine;
-use Digest::SHA;
-use File::Which;
my $xdg_cache_dir = "$ENV{HOME}/.cache";
$folder->close();
}
-# Match files by size and SHA-256; then delete duplicates
-sub builtin_remove_dups($) {
- my ($maildir) = @_;
- my (%size_to_files, %sha_to_files);
-
- # Group files by matching sizes
- foreach my $file (glob("$maildir/cur/*")) {
- my $size = -s $file;
- push(@{$size_to_files{$size}}, $file) if $size;
- }
-
- foreach my $same_size_files (values %size_to_files) {
- # Don't run sha unless there is another file of the same size
- next if scalar(@$same_size_files) < 2;
- %sha_to_files = ();
-
- # Group files with matching sizes by SHA-256
- foreach my $file (@$same_size_files) {
- open(my $fh, '<', $file) or next;
- binmode($fh);
- my $sha256hash = Digest::SHA->new(256)->addfile($fh)->hexdigest;
- close($fh);
-
- push(@{$sha_to_files{$sha256hash}}, $file);
- }
-
- # Remove duplicates
- foreach my $same_sha_files (values %sha_to_files) {
- next if scalar(@$same_sha_files) < 2;
- unlink(@{$same_sha_files}[1..$#$same_sha_files]);
- }
- }
-}
-
-# Use either fdupes or the built-in scanner to detect and remove duplicate
-# search results in the maildir
-sub remove_duplicates($) {
- my ($maildir) = @_;
-
- my $fdupes = which("fdupes");
- if ($fdupes) {
- system("$fdupes --hardlinks --symlinks --delete --noprompt"
- . " --quiet $maildir/cur/ > /dev/null");
- } else {
- builtin_remove_dups($maildir);
- }
-}
-
# search($maildir, $remove_dups, $query)
# search mails according to $query with notmuch; store results in $maildir
sub search($$$) {
my ($maildir, $remove_dups, $query) = @_;
+ my $dup_option = "";
+
$query = shell_quote($query);
+ if ($remove_dups) {
+ $dup_option = "--duplicate=1";
+ }
+
empty_maildir($maildir);
- system("notmuch search --output=files $query"
+ system("notmuch search --output=files $dup_option $query"
. " | sed -e 's: :\\\\ :g'"
. " | xargs --no-run-if-empty ln -s -t $maildir/cur/");
- remove_duplicates($maildir) if ($remove_dups);
}
sub prompt($$) {
my $mid = get_message_id();
defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n";
- system("notmuch tag "
- . shell_quote(join(' ', @_))
- . " id:$mid");
+ system("notmuch", "tag", @_, "--", "id:$mid");
}
sub die_usage() {
=item --remove-dups
-Remove duplicates from search results.
+Remove emails with duplicate message-ids from search results. (Passes
+--duplicate=1 to notmuch search command.) Note this can hide search
+results if an email accidentally or maliciously uses the same message-id
+as a different email.
=item -h
+++ /dev/null
-NOTMUCH PICK
-
-Notmuch pick is an experimental threaded message view for the emacs
-interface. Each message is one line in the results and the thread
-structure is shown using UTF-8 box drawing characters (similar to
-Mutt's threaded view). It comes between search and show in terms of
-amount of output and can be useful for viewing both single threads and
-multiple threads.
-
-INSTALL
-
-Just copy the notmuch-pick.el file somewhere into emacs's load-path.
-
-Then after the "(require 'notmuch)" line in your .emacs file add
-the line "(require 'notmuch-pick nil t)". This will load notmuch-pick on
-your next emacs start.
-
-TEST
-
-Just execute run-tests.sh and it should all work (it does require that
-notmuch has already been built).
-
-USING PICK
-
-The main key entries to notmuch pick are
-
-'z' enter a query to view using notmuch pick (works in hello, search,
- show and pick itself).
-'Z' view the current query in pick (works from search and show)
-'M-RET' view the selected thread in pick (works in search mode)
-
-Once in pick mode, keybindings are mostly in line with the rest of
-notmuch and are all viewable with '?' as usual.
-
-CUSTOMISING PICK
-
-Pick has several customisation variables. The most significant is the
-first notmuch-pick-show-out which determines the behaviour when
-selecting a message (with RET) in the pick view. By default pick uses
-a split window showing the single message in the bottom pane. However,
-if this option is set then it views the whole thread in the complete
-window jumping to the selected message in the thread. In either case
-M-RET selects the other option.
+++ /dev/null
-TODO FOR NOTMUCH-PICK
-
-(These are the things I can think of: to be added to as problems get
-reported or found!)
-
-Things that need fixing before acceptance to mainline
-
-- Review lisp to make idiomatic.
-- Unify functions with search or show where appropriate.
-- Work out a fall-back if the font does not contain box graphic characters.
-- Add extra functionality?
-
-- Remove debugging information.
-
-- Add tests (I have some but I am not sure how to add them if pick is
- in contrib).
-
-Bugs:
-
-- The display flickers while pick is running. I have no idea why.
-
-Other todo items
-
-- c i, c f for stashing ids etc.
-
-- Perhaps the author should be "To: ???" if the message is from the user.
-
-- Is there some nice way to do use the expand citation buttons of
- notmuch-show when using the split-pane mode?
+++ /dev/null
-;; notmuch-pick.el --- displaying notmuch forests.
-;;
-;; Copyright © Carl Worth
-;; Copyright © David Edmondson
-;; Copyright © Mark Walters
-;;
-;; This file is part of Notmuch.
-;;
-;; Notmuch is free software: you can redistribute it and/or modify it
-;; under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-;;
-;; Notmuch is distributed in the hope that it will be useful, but
-;; WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-;; General Public License for more details.
-;;
-;; You should have received a copy of the GNU General Public License
-;; along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
-;;
-;; Authors: David Edmondson <dme@dme.org>
-;; Mark Walters <markwalters1009@gmail.com>
-
-(require 'mail-parse)
-
-(require 'notmuch-lib)
-(require 'notmuch-query)
-(require 'notmuch-show)
-(require 'notmuch) ;; XXX ATM, as notmuch-search-mode-map is defined here
-
-(eval-when-compile (require 'cl))
-
-(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
-(declare-function notmuch-show "notmuch-show" (&rest args))
-(declare-function notmuch-tag "notmuch" (query &rest tags))
-(declare-function notmuch-show-strip-re "notmuch-show" (subject))
-(declare-function notmuch-show-spaces-n "notmuch-show" (n))
-(declare-function notmuch-read-query "notmuch" (prompt))
-(declare-function notmuch-read-tag-changes "notmuch" (&optional initial-input &rest search-terms))
-(declare-function notmuch-update-tags "notmuch" (current-tags tag-changes))
-(declare-function notmuch-hello-trim "notmuch-hello" (search))
-(declare-function notmuch-search-find-thread-id "notmuch" ())
-(declare-function notmuch-search-find-subject "notmuch" ())
-
-;; the following variable is defined in notmuch.el
-(defvar notmuch-search-query-string)
-
-(defgroup notmuch-pick nil
- "Showing message and thread structure."
- :group 'notmuch)
-
-;; This is ugly. We can't run setup-show-out until it has been defined
-;; which needs the keymap to be defined. So we defer setting up to
-;; notmuch-pick-init.
-(defcustom notmuch-pick-show-out nil
- "View selected messages in new window rather than split-pane."
- :type 'boolean
- :group 'notmuch-pick
- :set (lambda (symbol value)
- (set-default symbol value)
- (when (fboundp 'notmuch-pick-setup-show-out)
- (notmuch-pick-setup-show-out))))
-
-(defcustom notmuch-pick-result-format
- `(("date" . "%12s ")
- ("authors" . "%-20s")
- ("subject" . " %-54s ")
- ("tags" . "(%s)"))
- "Result formatting for Pick. Supported fields are: date,
- authors, subject, tags Note: subject includes the tree
- structure graphics, and the author string should not
- contain whitespace (put it in the neighbouring fields
- instead). For example:
- (setq notmuch-pick-result-format \(\(\"authors\" . \"%-40s\"\)
- \(\"subject\" . \"%s\"\)\)\)"
- :type '(alist :key-type (string) :value-type (string))
- :group 'notmuch-pick)
-
-(defcustom notmuch-pick-asynchronous-parser t
- "Use the asynchronous parser."
- :type 'boolean
- :group 'notmuch-pick)
-
-;; Faces for messages that match the query.
-(defface notmuch-pick-match-date-face
- '((t :inherit default))
- "Face used in pick mode for the date in messages matching the query."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-(defface notmuch-pick-match-author-face
- '((((class color)
- (background dark))
- (:foreground "OliveDrab1"))
- (((class color)
- (background light))
- (:foreground "dark blue"))
- (t
- (:bold t)))
- "Face used in pick mode for the date in messages matching the query."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-(defface notmuch-pick-match-subject-face
- '((t :inherit default))
- "Face used in pick mode for the subject in messages matching the query."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-(defface notmuch-pick-match-tag-face
- '((((class color)
- (background dark))
- (:foreground "OliveDrab1"))
- (((class color)
- (background light))
- (:foreground "navy blue" :bold t))
- (t
- (:bold t)))
- "Face used in pick mode for tags in messages matching the query."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-;; Faces for messages that do not match the query.
-(defface notmuch-pick-no-match-date-face
- '((t (:foreground "gray")))
- "Face used in pick mode for non-matching dates."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-(defface notmuch-pick-no-match-subject-face
- '((t (:foreground "gray")))
- "Face used in pick mode for non-matching subjects."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-(defface notmuch-pick-no-match-author-face
- '((t (:foreground "gray")))
- "Face used in pick mode for the date in messages matching the query."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-(defface notmuch-pick-no-match-tag-face
- '((t (:foreground "gray")))
- "Face used in pick mode face for non-matching tags."
- :group 'notmuch-pick
- :group 'notmuch-faces)
-
-(defvar notmuch-pick-previous-subject
- "The subject of the most recent result shown during the async display")
-(make-variable-buffer-local 'notmuch-pick-previous-subject)
-
-(defvar notmuch-pick-basic-query nil
- "A buffer local copy of argument query to the function notmuch-pick")
-(make-variable-buffer-local 'notmuch-pick-basic-query)
-
-(defvar notmuch-pick-query-context nil
- "A buffer local copy of argument query-context to the function notmuch-pick")
-(make-variable-buffer-local 'notmuch-pick-query-context)
-
-(defvar notmuch-pick-target-msg nil
- "A buffer local copy of argument target to the function notmuch-pick")
-(make-variable-buffer-local 'notmuch-pick-target-msg)
-
-(defvar notmuch-pick-open-target nil
- "A buffer local copy of argument open-target to the function notmuch-pick")
-(make-variable-buffer-local 'notmuch-pick-open-target)
-
-(defvar notmuch-pick-buffer-name nil
- "A buffer local copy of argument buffer-name to the function notmuch-pick")
-(make-variable-buffer-local 'notmuch-pick-buffer-name)
-
-(defvar notmuch-pick-message-window nil
- "The window of the message pane.
-
-It is set in both the pick buffer and the child show buffer. It
-is used to try and close the message pane when quitting pick or
-the child show buffer.")
-(make-variable-buffer-local 'notmuch-pick-message-window)
-(put 'notmuch-pick-message-window 'permanent-local t)
-
-(defvar notmuch-pick-message-buffer nil
- "The buffer name of the show buffer in the message pane.
-
-This is used to try and make sure we don't close the message pane
-if the user has loaded a different buffer in that window.")
-(make-variable-buffer-local 'notmuch-pick-message-buffer)
-(put 'notmuch-pick-message-buffer 'permanent-local t)
-
-(defvar notmuch-pick-mode-map
- (let ((map (make-sparse-keymap)))
- (define-key map [mouse-1] 'notmuch-pick-show-message)
- (define-key map "q" 'notmuch-pick-quit)
- (define-key map "x" 'notmuch-pick-quit)
- (define-key map "?" 'notmuch-help)
- (define-key map "a" 'notmuch-pick-archive-message-then-next)
- (define-key map "=" 'notmuch-pick-refresh-view)
- (define-key map "s" 'notmuch-pick-to-search)
- (define-key map "z" 'notmuch-pick-to-pick)
- (define-key map "m" 'notmuch-pick-new-mail)
- (define-key map "f" 'notmuch-pick-forward-message)
- (define-key map "r" 'notmuch-pick-reply-sender)
- (define-key map "R" 'notmuch-pick-reply)
- (define-key map "n" 'notmuch-pick-next-matching-message)
- (define-key map "p" 'notmuch-pick-prev-matching-message)
- (define-key map "N" 'notmuch-pick-next-message)
- (define-key map "P" 'notmuch-pick-prev-message)
- (define-key map "|" 'notmuch-pick-pipe-message)
- (define-key map "-" 'notmuch-pick-remove-tag)
- (define-key map "+" 'notmuch-pick-add-tag)
- (define-key map " " 'notmuch-pick-scroll-or-next)
- (define-key map "b" 'notmuch-pick-scroll-message-window-back)
- map))
-(fset 'notmuch-pick-mode-map notmuch-pick-mode-map)
-
-(defun notmuch-pick-setup-show-out ()
- "Set up the keymap for showing a thread
-
-This uses the value of the defcustom notmuch-pick-show-out to
-decide whether to show a message in the message pane or in the
-whole window."
- (let ((map notmuch-pick-mode-map))
- (if notmuch-pick-show-out
- (progn
- (define-key map (kbd "M-RET") 'notmuch-pick-show-message)
- (define-key map (kbd "RET") 'notmuch-pick-show-message-out))
- (progn
- (define-key map (kbd "RET") 'notmuch-pick-show-message)
- (define-key map (kbd "M-RET") 'notmuch-pick-show-message-out)))))
-
-(defun notmuch-pick-get-message-properties ()
- "Return the properties of the current message as a plist.
-
-Some useful entries are:
-:headers - Property list containing the headers :Date, :Subject, :From, etc.
-:tags - Tags for this message"
- (save-excursion
- (beginning-of-line)
- (get-text-property (point) :notmuch-message-properties)))
-
-(defun notmuch-pick-set-message-properties (props)
- (save-excursion
- (beginning-of-line)
- (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
-
-(defun notmuch-pick-set-prop (prop val &optional props)
- (let ((inhibit-read-only t)
- (props (or props
- (notmuch-pick-get-message-properties))))
- (plist-put props prop val)
- (notmuch-pick-set-message-properties props)))
-
-(defun notmuch-pick-get-prop (prop &optional props)
- (let ((props (or props
- (notmuch-pick-get-message-properties))))
- (plist-get props prop)))
-
-(defun notmuch-pick-set-tags (tags)
- "Set the tags of the current message."
- (notmuch-pick-set-prop :tags tags))
-
-(defun notmuch-pick-get-tags ()
- "Return the tags of the current message."
- (notmuch-pick-get-prop :tags))
-
-(defun notmuch-pick-get-message-id ()
- "Return the message id of the current message."
- (let ((id (notmuch-pick-get-prop :id)))
- (if id
- (notmuch-id-to-query id)
- nil)))
-
-(defun notmuch-pick-get-match ()
- "Return whether the current message is a match."
- (interactive)
- (notmuch-pick-get-prop :match))
-
-(defun notmuch-pick-refresh-result ()
- "Redisplay the current message line.
-
-This redisplays the current line based on the messages
-properties (as they are now). This is used when tags are
-updated."
- (let ((init-point (point))
- (end (line-end-position))
- (msg (notmuch-pick-get-message-properties))
- (inhibit-read-only t))
- (beginning-of-line)
- ;; This is a little tricky: we override
- ;; notmuch-pick-previous-subject to get the decision between
- ;; ... and a subject right and it stops notmuch-pick-insert-msg
- ;; from overwriting the buffer local copy of
- ;; notmuch-pick-previous-subject if this is called while the
- ;; buffer is displaying.
- (let ((notmuch-pick-previous-subject (notmuch-pick-get-prop :previous-subject)))
- (delete-region (point) (1+ (line-end-position)))
- (notmuch-pick-insert-msg msg))
- (let ((new-end (line-end-position)))
- (goto-char (if (= init-point end)
- new-end
- (min init-point (- new-end 1)))))))
-
-(defun notmuch-pick-tag-update-display (&optional tag-changes)
- "Update display for TAG-CHANGES to current message.
-
-Does NOT change the database."
- (let* ((current-tags (notmuch-pick-get-tags))
- (new-tags (notmuch-update-tags current-tags tag-changes)))
- (unless (equal current-tags new-tags)
- (notmuch-pick-set-tags new-tags)
- (notmuch-pick-refresh-result))))
-
-(defun notmuch-pick-tag (&optional tag-changes)
- "Change tags for the current message"
- (interactive)
- (setq tag-changes (notmuch-tag (notmuch-pick-get-message-id) tag-changes))
- (notmuch-pick-tag-update-display tag-changes))
-
-(defun notmuch-pick-add-tag ()
- "Same as `notmuch-pick-tag' but sets initial input to '+'."
- (interactive)
- (notmuch-pick-tag "+"))
-
-(defun notmuch-pick-remove-tag ()
- "Same as `notmuch-pick-tag' but sets initial input to '-'."
- (interactive)
- (notmuch-pick-tag "-"))
-
-;; The next two functions close the message window before searching or
-;; picking but they do so after the user has entered the query (in
-;; case the user was basing the query on something in the message
-;; window).
-
-(defun notmuch-pick-to-search ()
- "Run \"notmuch search\" with the given `query' and display results."
- (interactive)
- (let ((query (notmuch-read-query "Notmuch search: ")))
- (notmuch-pick-close-message-window)
- (notmuch-search query)))
-
-(defun notmuch-pick-to-pick ()
- "Run a query and display results in experimental notmuch-pick mode"
- (interactive)
- (let ((query (notmuch-read-query "Notmuch pick: ")))
- (notmuch-pick-close-message-window)
- (notmuch-pick query)))
-
-;; This function should be in notmuch-hello.el but we are trying to
-;; minimise impact on the rest of the codebase.
-(defun notmuch-pick-from-hello (&optional search)
- "Run a query and display results in experimental notmuch-pick mode"
- (interactive)
- (unless (null search)
- (setq search (notmuch-hello-trim search))
- (let ((history-delete-duplicates t))
- (add-to-history 'notmuch-search-history search)))
- (notmuch-pick search))
-
-;; This function should be in notmuch-show.el but be we trying to
-;; minimise impact on the rest of the codebase.
-(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-show-get-message-id)))
-
-;; This function should be in notmuch.el but be we trying to minimise
-;; impact on the rest of the codebase.
-(defun notmuch-pick-from-search-current-query ()
- "Call notmuch pick with the current query"
- (interactive)
- (notmuch-pick notmuch-search-query-string))
-
-;; This function should be in notmuch.el but be we trying to minimise
-;; impact on the rest of the codebase.
-(defun notmuch-pick-from-search-thread ()
- "Show the selected thread with notmuch-pick"
- (interactive)
- (notmuch-pick (notmuch-search-find-thread-id)
- notmuch-search-query-string
- nil
- (notmuch-prettify-subject (notmuch-search-find-subject))
- t))
-
-(defun notmuch-pick-message-window-kill-hook ()
- "Close the message pane when exiting the show buffer."
- (let ((buffer (current-buffer)))
- (when (and (window-live-p notmuch-pick-message-window)
- (eq (window-buffer notmuch-pick-message-window) buffer))
- ;; We do not want an error if this is the sole window in the
- ;; frame and I do not know how to test for that in emacs pre
- ;; 24. Hence we just ignore-errors.
- (ignore-errors
- (delete-window notmuch-pick-message-window)))))
-
-(defun notmuch-pick-show-message ()
- "Show the current message (in split-pane)."
- (interactive)
- (let ((id (notmuch-pick-get-message-id))
- (inhibit-read-only t)
- buffer)
- (when id
- ;; We close and reopen the window to kill off un-needed buffers
- ;; this might cause flickering but seems ok.
- (notmuch-pick-close-message-window)
- (setq notmuch-pick-message-window
- (split-window-vertically (/ (window-height) 4)))
- (with-selected-window notmuch-pick-message-window
- ;; Since we are only displaying one message do not indent.
- (let ((notmuch-show-indent-messages-width 0)
- (notmuch-show-only-matching-messages t))
- (setq buffer (notmuch-show id nil nil nil))))
- ;; We need the `let' as notmuch-pick-message-window is buffer local.
- (let ((window notmuch-pick-message-window))
- (with-current-buffer buffer
- (setq notmuch-pick-message-window window)
- (add-hook 'kill-buffer-hook 'notmuch-pick-message-window-kill-hook)))
- (when notmuch-show-mark-read-tags
- (notmuch-pick-tag-update-display notmuch-show-mark-read-tags))
- (setq notmuch-pick-message-buffer buffer))))
-
-(defun notmuch-pick-show-message-out ()
- "Show the current message (in whole window)."
- (interactive)
- (let ((id (notmuch-pick-get-message-id))
- (inhibit-read-only t)
- buffer)
- (when id
- ;; We close the window to kill off un-needed buffers.
- (notmuch-pick-close-message-window)
- (notmuch-show id nil nil nil))))
-
-(defun notmuch-pick-scroll-message-window ()
- "Scroll the message window (if it exists)"
- (interactive)
- (when (window-live-p notmuch-pick-message-window)
- (with-selected-window notmuch-pick-message-window
- (if (pos-visible-in-window-p (point-max))
- t
- (scroll-up)))))
-
-(defun notmuch-pick-scroll-message-window-back ()
- "Scroll the message window back(if it exists)"
- (interactive)
- (when (window-live-p notmuch-pick-message-window)
- (with-selected-window notmuch-pick-message-window
- (if (pos-visible-in-window-p (point-min))
- t
- (scroll-down)))))
-
-(defun notmuch-pick-scroll-or-next ()
- "Scroll the message window. If it at end go to next message."
- (interactive)
- (when (notmuch-pick-scroll-message-window)
- (notmuch-pick-next-matching-message)))
-
-(defun notmuch-pick-quit ()
- "Close the split view or exit pick."
- (interactive)
- (unless (notmuch-pick-close-message-window)
- (kill-buffer (current-buffer))))
-
-(defun notmuch-pick-close-message-window ()
- "Close the message-window. Return t if close succeeds."
- (interactive)
- (when (and (window-live-p notmuch-pick-message-window)
- (eq (window-buffer notmuch-pick-message-window) notmuch-pick-message-buffer))
- (delete-window notmuch-pick-message-window)
- (unless (get-buffer-window-list notmuch-pick-message-buffer)
- (kill-buffer notmuch-pick-message-buffer))
- t))
-
-(defun notmuch-pick-archive-message (&optional unarchive)
- "Archive the current message.
-
-Archive the current message by applying the tag changes in
-`notmuch-archive-tags' to it. If a prefix argument is given, the
-message will be \"unarchived\", i.e. the tag changes in
-`notmuch-archive-tags' will be reversed."
- (interactive "P")
- (when notmuch-archive-tags
- (apply 'notmuch-pick-tag
- (notmuch-tag-change-list notmuch-archive-tags unarchive))))
-
-(defun notmuch-pick-archive-message-then-next (&optional unarchive)
- "Archive the current message and move to next matching message."
- (interactive "P")
- (notmuch-pick-archive-message unarchive)
- (notmuch-pick-next-matching-message))
-
-(defun notmuch-pick-next-message ()
- "Move to next message."
- (interactive)
- (forward-line)
- (when (window-live-p notmuch-pick-message-window)
- (notmuch-pick-show-message)))
-
-(defun notmuch-pick-prev-message ()
- "Move to previous message."
- (interactive)
- (forward-line -1)
- (when (window-live-p notmuch-pick-message-window)
- (notmuch-pick-show-message)))
-
-(defun notmuch-pick-prev-matching-message ()
- "Move to previous matching message."
- (interactive)
- (forward-line -1)
- (while (and (not (bobp)) (not (notmuch-pick-get-match)))
- (forward-line -1))
- (when (window-live-p notmuch-pick-message-window)
- (notmuch-pick-show-message)))
-
-(defun notmuch-pick-next-matching-message ()
- "Move to next matching message."
- (interactive)
- (forward-line)
- (while (and (not (eobp)) (not (notmuch-pick-get-match)))
- (forward-line))
- (when (window-live-p notmuch-pick-message-window)
- (notmuch-pick-show-message)))
-
-(defun notmuch-pick-refresh-view ()
- "Refresh view."
- (interactive)
- (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
- 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"
- `(save-excursion
- (let ((id (notmuch-pick-get-message-id)))
- (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
- (with-current-buffer buf
- (call-process notmuch-command nil t nil "show" "--format=raw" id)
- ,@body)
- (kill-buffer buf)))))
-
-(defun notmuch-pick-new-mail (&optional prompt-for-sender)
- "Compose new mail."
- (interactive "P")
- (notmuch-pick-close-message-window)
- (notmuch-mua-new-mail prompt-for-sender ))
-
-(defun notmuch-pick-forward-message (&optional prompt-for-sender)
- "Forward the current message."
- (interactive "P")
- (notmuch-pick-close-message-window)
- (with-current-notmuch-pick-message
- (notmuch-mua-new-forward-message prompt-for-sender)))
-
-(defun notmuch-pick-reply (&optional prompt-for-sender)
- "Reply to the sender and all recipients of the current message."
- (interactive "P")
- (notmuch-pick-close-message-window)
- (notmuch-mua-new-reply (notmuch-pick-get-message-id) prompt-for-sender t))
-
-(defun notmuch-pick-reply-sender (&optional prompt-for-sender)
- "Reply to the sender of the current message."
- (interactive "P")
- (notmuch-pick-close-message-window)
- (notmuch-mua-new-reply (notmuch-pick-get-message-id) prompt-for-sender nil))
-
-;; Shamelessly stolen from notmuch-show.el: maybe should be unified.
-(defun notmuch-pick-pipe-message (command)
- "Pipe the contents of the current message to the given command.
-
-The given command will be executed with the raw contents of the
-current email message as stdin. Anything printed by the command
-to stdout or stderr will appear in the *notmuch-pipe* buffer.
-
-When invoked with a prefix argument, the command will receive all
-open messages in the current thread (formatted as an mbox) rather
-than only the current message."
- (interactive "sPipe message to command: ")
- (let ((shell-command
- (concat notmuch-command " show --format=raw "
- (shell-quote-argument (notmuch-pick-get-message-id)) " | " command))
- (buf (get-buffer-create (concat "*notmuch-pipe*"))))
- (with-current-buffer buf
- (setq buffer-read-only nil)
- (erase-buffer)
- (let ((exit-code (call-process-shell-command shell-command nil buf)))
- (goto-char (point-max))
- (set-buffer-modified-p nil)
- (setq buffer-read-only t)
- (unless (zerop exit-code)
- (switch-to-buffer-other-window buf)
- (message (format "Command '%s' exited abnormally with code %d"
- shell-command exit-code)))))))
-
-(defun notmuch-pick-clean-address (address)
- "Try to clean a single email ADDRESS for display. Return
-AUTHOR_NAME if present, otherwise return AUTHOR_EMAIL. Return
-unchanged ADDRESS if parsing fails."
- (let* ((clean-address (notmuch-clean-address address))
- (p-address (car clean-address))
- (p-name (cdr clean-address)))
-
- ;; If we have a name return that otherwise return the address.
- (or p-name p-address)))
-
-(defun notmuch-pick-insert-field (field format-string msg)
- (let* ((headers (plist-get msg :headers))
- (match (plist-get msg :match)))
- (cond
- ((string-equal field "date")
- (let ((face (if match
- 'notmuch-pick-match-date-face
- 'notmuch-pick-no-match-date-face)))
- (insert (propertize (format format-string (plist-get msg :date_relative))
- 'face face))))
-
- ((string-equal field "subject")
- (let ((tree-status (plist-get msg :tree-status))
- (bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))
- (face (if match
- 'notmuch-pick-match-subject-face
- 'notmuch-pick-no-match-subject-face)))
- (insert (propertize (format format-string
- (concat
- (mapconcat #'identity (reverse tree-status) "")
- (if (string= notmuch-pick-previous-subject bare-subject)
- " ..."
- bare-subject)))
- 'face face))
- (setq notmuch-pick-previous-subject bare-subject)))
-
- ((string-equal field "authors")
- (let ((author (notmuch-pick-clean-address (plist-get headers :From)))
- (len (length (format format-string "")))
- (face (if match
- 'notmuch-pick-match-author-face
- 'notmuch-pick-no-match-author-face)))
- (when (> (length author) len)
- (setq author (substring author 0 len)))
- (insert (propertize (format format-string author)
- 'face face))))
-
- ((string-equal field "tags")
- (let ((tags (plist-get msg :tags))
- (face (if match
- 'notmuch-pick-match-tag-face
- 'notmuch-pick-no-match-tag-face)))
- (when tags
- (insert (propertize (format format-string
- (mapconcat #'identity tags ", "))
- 'face face))))))))
-
-(defun notmuch-pick-insert-msg (msg)
- "Insert the message MSG according to notmuch-pick-result-format"
- ;; We need to save the previous subject as it will get overwritten
- ;; by the insert-field calls.
- (let ((previous-subject notmuch-pick-previous-subject))
- (dolist (spec notmuch-pick-result-format)
- (notmuch-pick-insert-field (car spec) (cdr spec) msg))
- (notmuch-pick-set-message-properties msg)
- (notmuch-pick-set-prop :previous-subject previous-subject)
- (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)))
- (target notmuch-pick-target-msg))
- (when (or (and (not target) (plist-get msg :match))
- (string= msg-id target))
- (setq notmuch-pick-target-msg "found")
- (goto-char (point-max))
- (forward-line -1)
- (when notmuch-pick-open-target
- (notmuch-pick-show-message)))))
-
-(defun notmuch-pick-insert-tree (tree depth tree-status first last)
- "Insert the message tree TREE at depth DEPTH in the current thread.
-
-A message tree is another name for a single sub-thread: i.e., a
-message together with all its descendents."
- (let ((msg (car tree))
- (replies (cadr tree)))
-
- (cond
- ((and (< 0 depth) (not last))
- (push "├" tree-status))
- ((and (< 0 depth) last)
- (push "╰" tree-status))
- ((and (eq 0 depth) first last)
-;; (push "─" tree-status)) choice between this and next line is matter of taste.
- (push " " tree-status))
- ((and (eq 0 depth) first (not last))
- (push "┬" tree-status))
- ((and (eq 0 depth) (not first) last)
- (push "╰" tree-status))
- ((and (eq 0 depth) (not first) (not last))
- (push "├" tree-status)))
-
- (push (concat (if replies "┬" "─") "►") tree-status)
- (notmuch-pick-goto-and-insert-msg (plist-put msg :tree-status tree-status))
- (pop tree-status)
- (pop tree-status)
-
- (if last
- (push " " tree-status)
- (push "│" tree-status))
-
- (notmuch-pick-insert-thread replies (1+ depth) tree-status)))
-
-(defun notmuch-pick-insert-thread (thread depth tree-status)
- "Insert the collection of sibling sub-threads THREAD at depth DEPTH in the current forest."
- (let ((n (length thread)))
- (loop for tree in thread
- for count from 1 to n
-
- do (notmuch-pick-insert-tree tree depth tree-status (eq count 1) (eq count n)))))
-
-(defun notmuch-pick-insert-forest-thread (forest-thread)
- "Insert a single complete thread."
- (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)
- "Insert a forest of threads.
-
-This function inserts a collection of several complete threads as
-passed to it by notmuch-pick-process-filter."
- (mapc 'notmuch-pick-insert-forest-thread forest))
-
-(defun notmuch-pick-mode ()
- "Major mode displaying messages (as opposed to threads) of of a notmuch search.
-
-This buffer contains the results of a \"notmuch pick\" of your
-email archives. Each line in the buffer represents a single
-message giving the relative date, the author, subject, and any
-tags.
-
-Pressing \\[notmuch-pick-show-message] on any line displays that message.
-
-Complete list of currently available key bindings:
-
-\\{notmuch-pick-mode-map}"
-
- (interactive)
- (kill-all-local-variables)
- (use-local-map notmuch-pick-mode-map)
- (setq major-mode 'notmuch-pick-mode
- mode-name "notmuch-pick")
- (hl-line-mode 1)
- (setq buffer-read-only t
- truncate-lines t))
-
-(defun notmuch-pick-process-sentinel (proc msg)
- "Add a message to let user know when \"notmuch pick\" exits"
- (let ((buffer (process-buffer proc))
- (status (process-status proc))
- (exit-status (process-exit-status proc))
- (never-found-target-thread nil))
- (when (memq status '(exit signal))
- (kill-buffer (process-get proc 'parse-buf))
- (if (buffer-live-p buffer)
- (with-current-buffer buffer
- (save-excursion
- (let ((inhibit-read-only t)
- (atbob (bobp)))
- (goto-char (point-max))
- (if (eq status 'signal)
- (insert "Incomplete search results (pick process was killed).\n"))
- (when (eq status 'exit)
- (insert "End of search results.")
- (unless (= exit-status 0)
- (insert (format " (process returned %d)" exit-status)))
- (insert "\n")))))))))
-
-(defun notmuch-pick-process-filter (proc string)
- "Process and filter the output of \"notmuch show\" (for pick)"
- (let ((results-buf (process-buffer proc))
- (parse-buf (process-get proc 'parse-buf))
- (inhibit-read-only t)
- done)
- (if (not (buffer-live-p results-buf))
- (delete-process proc)
- (with-current-buffer parse-buf
- ;; Insert new data
- (save-excursion
- (goto-char (point-max))
- (insert string))
- (notmuch-sexp-parse-partial-list 'notmuch-pick-insert-forest-thread
- results-buf)))))
-
-(defun notmuch-pick-worker (basic-query &optional query-context target buffer open-target)
- (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)
- (setq notmuch-pick-open-target open-target)
-
- (erase-buffer)
- (goto-char (point-min))
- (let* ((search-args (concat basic-query
- (if query-context (concat " and (" query-context ")"))
- ))
- (message-arg "--entire-thread"))
- (if (equal (car (process-lines notmuch-command "count" search-args)) "0")
- (setq search-args basic-query))
- (if notmuch-pick-asynchronous-parser
- (let ((proc (notmuch-start-notmuch
- "notmuch-pick" buffer #'notmuch-pick-process-sentinel
- "show" "--body=false" "--format=sexp"
- message-arg search-args))
- ;; Use a scratch buffer to accumulate partial output.
- ;; This buffer will be killed by the sentinel, which
- ;; should be called no matter how the process dies.
- (parse-buf (generate-new-buffer " *notmuch pick parse*")))
- (process-put proc 'parse-buf parse-buf)
- (set-process-filter proc 'notmuch-pick-process-filter)
- (set-process-query-on-exit-flag proc nil))
- (progn
- (notmuch-pick-insert-forest
- (notmuch-query-get-threads
- (list "--body=false" message-arg search-args)))
- (save-excursion
- (goto-char (point-max))
- (insert "End of search results.\n"))))))
-
-
-(defun notmuch-pick (&optional query query-context target buffer-name open-target)
- "Run notmuch pick with the given `query' and display the results.
-
-The arguments are:
- QUERY: the main query. This can be any query but in many cases will be
- a single thread. If nil this is read interactively from the minibuffer.
- QUERY-CONTEXT: is an additional term for the query. The query used
- is QUERY and QUERY-CONTEXT unless that does not match any messages
- in which case we fall back to just QUERY.
- TARGET: A message ID (with the id: prefix) that will be made
- current if it appears in the pick results.
- BUFFER-NAME: the name of the buffer to show the pick tree. If
- it is nil \"*notmuch-pick\" followed by QUERY is used.
- OPEN-TARGET: If TRUE open the target message in the message pane."
- (interactive "sNotmuch pick: ")
- (if (null query)
- (setq query (notmuch-read-query "Notmuch pick: ")))
- (let ((buffer (get-buffer-create (generate-new-buffer-name
- (or buffer-name
- (concat "*notmuch-pick-" query "*")))))
- (inhibit-read-only t))
-
- (switch-to-buffer buffer)
- ;; Don't track undo information for this buffer
- (set 'buffer-undo-list t)
-
- (notmuch-pick-worker query query-context target buffer open-target)
-
- (setq truncate-lines t)))
-
-
-;; Set up key bindings from the rest of notmuch.
-(define-key 'notmuch-search-mode-map "z" 'notmuch-pick)
-(define-key 'notmuch-search-mode-map "Z" 'notmuch-pick-from-search-current-query)
-(define-key 'notmuch-search-mode-map (kbd "M-RET") 'notmuch-pick-from-search-thread)
-(define-key 'notmuch-hello-mode-map "z" 'notmuch-pick-from-hello)
-(define-key 'notmuch-show-mode-map "z" 'notmuch-pick)
-(define-key 'notmuch-show-mode-map "Z" 'notmuch-pick-from-show-current-query)
-(notmuch-pick-setup-show-out)
-(message "Initialised notmuch-pick")
-
-(provide 'notmuch-pick)
+++ /dev/null
-#!/usr/bin/env bash
-
-set -eu
-
-fail() {
- echo ERROR $1
- exit 1
-}
-
-TESTS="emacs-pick emacs-pick-sync"
-TESTFILES="$TESTS pick.expected-output"
-
-export PICK_DIR="`cd \`dirname "$0"\` && pwd`"
-PICK_TEST_DIR="$PICK_DIR/test"
-
-
-for f in $TESTFILES
-do
- test -f "$PICK_TEST_DIR/$f" || test -d "$PICK_TEST_DIR/$f" || fail "$PICK_TEST_DIR/$f does not exist"
-done
-
-cd "$PICK_DIR/../../test"
-
-test -x ../notmuch || fail "`cd .. && pwd`/notmuch has not been built"
-
-for f in $TESTFILES
-do
- if test -f "$f"
- then
- fail "$f exists"
- fi
-done
-
-trap "rm -f $TESTFILES" 0
-
-for f in $TESTFILES
-do
- ln -s "$PICK_TEST_DIR/$f" .
-done
-
-#don't exec -- traps would not run.
-for f in $TESTS
-do
- echo $f
- ./$f
-done
+++ /dev/null
-#!/usr/bin/env bash
-
-test_description="emacs pick interface"
-. test-lib.sh
-
-EXPECTED=$TEST_DIRECTORY/pick.expected-output
-
-add_email_corpus
-test_begin_subtest "Do we have emacs"
-test_emacs '(insert "hello\n")
- (test-output)'
-cat <<EOF >EXPECTED
-hello
-EOF
-test_expect_equal_file OUTPUT EXPECTED
-
-test_begin_subtest "Basic notmuch-pick view in emacs"
-test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
- (require (quote notmuch-pick))
- (notmuch-pick "tag:inbox")
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-tag-inbox
-
-test_begin_subtest "Navigation of notmuch-hello to search results"
-test_emacs '(notmuch-hello)
- (goto-char (point-min))
- (re-search-forward "inbox")
- (widget-button-press (1- (point)))
- (notmuch-test-wait)
- (notmuch-pick-from-search-current-query)
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-tag-inbox
-
-test_begin_subtest "Pick of a single thread (from search)"
-test_emacs '(notmuch-hello)
- (goto-char (point-min))
- (re-search-forward "inbox")
- (widget-button-press (1- (point)))
- (notmuch-test-wait)
- (notmuch-pick-from-search-thread)
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
-
-test_begin_subtest "Pick of a single thread (from show)"
-test_emacs '(notmuch-hello)
- (goto-char (point-min))
- (re-search-forward "inbox")
- (widget-button-press (1- (point)))
- (notmuch-test-wait)
- (notmuch-search-show-thread)
- (notmuch-pick-from-show-current-query)
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
-
-test_begin_subtest "Message window of pick"
-test_emacs '(notmuch-hello)
- (goto-char (point-min))
- (re-search-forward "inbox")
- (widget-button-press (1- (point)))
- (notmuch-test-wait)
- (notmuch-search-next-thread)
- (notmuch-pick-from-search-thread)
- (notmuch-test-wait)
- (select-window notmuch-pick-message-window)
- (test-output)
- (delete-other-windows)'
-cp OUTPUT /tmp/mjwout
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-show-window
-
-test_done
+++ /dev/null
-#!/usr/bin/env bash
-
-test_description="emacs pick interface (sync parser)"
-. test-lib.sh
-
-EXPECTED=$TEST_DIRECTORY/pick.expected-output
-
-add_email_corpus
-test_begin_subtest "Do we have emacs"
-test_emacs '(insert "hello\n")
- (test-output)'
-cat <<EOF >EXPECTED
-hello
-EOF
-test_expect_equal_file OUTPUT EXPECTED
-
-test_begin_subtest "Basic notmuch-pick view in emacs"
-test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
- (require (quote notmuch-pick))
- (setq notmuch-pick-asynchronous-parser nil)
- (notmuch-pick "tag:inbox")
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-tag-inbox
-
-test_begin_subtest "Navigation of notmuch-hello to search results"
-test_emacs '(notmuch-hello)
- (goto-char (point-min))
- (re-search-forward "inbox")
- (widget-button-press (1- (point)))
- (notmuch-test-wait)
- (notmuch-pick-from-search-current-query)
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-tag-inbox
-
-test_begin_subtest "Pick of a single thread (from search)"
-test_emacs '(notmuch-hello)
- (goto-char (point-min))
- (re-search-forward "inbox")
- (widget-button-press (1- (point)))
- (notmuch-test-wait)
- (notmuch-pick-from-search-thread)
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
-
-test_begin_subtest "Pick of a single thread (from show)"
-test_emacs '(notmuch-hello)
- (goto-char (point-min))
- (re-search-forward "inbox")
- (widget-button-press (1- (point)))
- (notmuch-test-wait)
- (notmuch-search-show-thread)
- (notmuch-pick-from-show-current-query)
- (notmuch-test-wait)
- (test-output)
- (delete-other-windows)'
-test_expect_equal_file OUTPUT $EXPECTED/notmuch-pick-single-thread
-
-test_done
+++ /dev/null
-Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
-Subject: [notmuch] Working with Maildir storage?
-To: notmuch@notmuchmail.org
-Date: Tue, 17 Nov 2009 14:00:54 -0500
-
-[ multipart/mixed ]
-[ multipart/signed ]
-[ text/plain ]
-I saw the LWN article and decided to take a look at notmuch. I'm
-currently using mutt and mairix to index and read a collection of
-Maildir mail folders (around 40,000 messages total).
-
-notmuch indexed the messages without complaint, but my attempt at
-searching bombed out. Running, for example:
-
- notmuch search storage
-
-Resulted in 4604 lines of errors along the lines of:
-
- Error opening
- /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
- Too many open files
-
-I'm curious if this is expected behavior (i.e., notmuch does not work
-with Maildir) or if something else is going on.
-
-Cheers,
-
-[ 4-line signature. Click/Enter to show. ]
---
-Lars Kellogg-Stedman <lars@seas.harvard.edu>
-Senior Technologist, Computing and Information Technology
-Harvard University School of Engineering and Applied Sciences
-[ application/pgp-signature ]
-[ text/plain ]
-[ 4-line signature. Click/Enter to show. ]
-_______________________________________________
-notmuch mailing list
-notmuch@notmuchmail.org
-http://notmuchmail.org/mailman/listinfo/notmuch
+++ /dev/null
- 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox)
- 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++ file with gcc 4.4 (inbox, unread)
- 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
- 2009-11-17 Keith Packard ╰┬► ... (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
-End of search results.
+++ /dev/null
- 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox, unread)
- 2010-12-16 Olivier Berger ─►Essai accentué (inbox, unread)
- 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox, unread)
- 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment, inbox, unread)
- 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox, unread)
- 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox, signed, unread)
- 2009-11-18 Keith Packard ├─► ... (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox, unread)
- 2009-11-18 Keith Packard ├─► ... (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox, unread)
- 2009-11-17 Jan Janak ├─► ... (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox, unread)
- 2009-11-18 Keith Packard ╰┬► ... (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
- 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
- 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox, signed, unread)
- 2009-11-17 Mikhail Gusarov ├┬► ... (inbox, signed, unread)
- 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox, signed, unread)
- 2009-11-17 Mikhail Gusarov │ ├─► ... (inbox, unread)
- 2009-11-17 Keith Packard │ ╰┬► ... (inbox, unread)
- 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox, signed, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
- 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++ file with gcc 4.4 (inbox, unread)
- 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
- 2009-11-17 Keith Packard ╰┬► ... (inbox, unread)
- 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
- 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
- 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
- 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox, unread)
- 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox, unread)
- 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox, unread)
- 2009-11-18 Jjgod Jiang ╰┬► ... (inbox, unread)
- 2009-11-18 Alexander Botero-Low ╰─► ... (inbox, unread)
- 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox, unread)
- 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox, unread)
- 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox, unread)
- 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox, unread)
- 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox, unread)
- 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment, inbox, signed, unread)
- 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment, inbox, signed, unread)
- 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox, unread)
- 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment, inbox, unread)
- 2009-11-17 Carl Worth ╰─► ... (inbox, unread)
-End of search results.
--- /dev/null
+notmuch (0.17-1) unstable; urgency=low
+
+ * Previously on big endian architectures like sparc and powerpc the
+ computation of SHA1 hashes was incorrect. This meant that messages
+ with overlong or missing message-ids were given different computed
+ message-ids than on more common little endian architectures like
+ i386 and amd64. If you use notmuch on a big endian architecture,
+ you are strongly advised to make a backup of your tags using
+ `notmuch dump` before this upgrade. You can locate the affected
+ files using something like:
+
+ notmuch dump | \
+ awk '/^notmuch-sha1-[0-9a-f]{40} / \
+ {system("notmuch search --exclude=false --output=files id:" $1)}'
+
+ -- David Bremner <bremner@debian.org> Mon, 30 Dec 2013 20:31:16 -0400
+
+notmuch (0.16-1) unstable; urgency=low
+
+ * The vim interface has been rewritten from scratch. In particular
+ it requires a version of vim with ruby support.
+
+ -- David Bremner <bremner@debian.org> Sat, 16 Feb 2013 08:12:02 -0400
+
+notmuch (0.14-1) unstable; urgency=low
+
+ There is an incompatible change in option syntax for dump and restore
+ in this release. Please update your scripts.
+
+ From upstream NEWS:
+
+ The deprecated positional output file argument to notmuch dump has
+ been replaced with an --output option. The input file positional
+ argument for restore has been replaced with an --input option for
+ consistency with dump.
+
+ -- David Bremner <bremner@debian.org> Sun, 05 Aug 2012 11:52:49 -0300
+
+notmuch (0.6~238) unstable; urgency=low
+
+ The emacs user interface to notmuch is now contained in a separate
+ package called notmuch-emacs.
+
+ -- David Bremner <bremner@debian.org> Mon, 20 Jun 2011 23:57:55 -0300
+++ /dev/null
-notmuch (0.16-1) unstable; urgency=low
-
- * The vim interface has been rewritten from scratch. In particular
- it requires a version of vim with ruby support.
-
- -- David Bremner <bremner@debian.org> Sat, 16 Feb 2013 08:12:02 -0400
-
-notmuch (0.14-1) unstable; urgency=low
-
- There is an incompatible change in option syntax for dump and restore
- in this release. Please update your scripts.
-
- From upstream NEWS:
-
- The deprecated positional output file argument to notmuch dump has
- been replaced with an --output option. The input file positional
- argument for restore has been replaced with an --input option for
- consistency with dump.
-
- -- David Bremner <bremner@debian.org> Sun, 05 Aug 2012 11:52:49 -0300
-
-notmuch (0.6~238) unstable; urgency=low
-
- The emacs user interface to notmuch is now contained in a separate
- package called notmuch-emacs.
-
- -- David Bremner <bremner@debian.org> Mon, 20 Jun 2011 23:57:55 -0300
+notmuch (0.17-3) unstable; urgency=medium
+
+ * update notmuch-emacs for debian emacs policy 2.0.6
+ * Update emacs test suite for Hurd compatibility
+
+ -- David Bremner <bremner@debian.org> Sun, 12 Jan 2014 17:07:16 -0400
+
+notmuch (0.17-2) unstable; urgency=medium
+
+ * Bug fix: "package should warn in a NEWS.Debian file about possible
+ pre-upgrade action", thanks to Jonas Smedegaard (Closes: #733853).
+
+ -- David Bremner <bremner@debian.org> Wed, 01 Jan 2014 07:44:25 -0400
+
+notmuch (0.17-1) unstable; urgency=low
+
+ * New upstream feature release. See /usr/share/doc/notmuch/NEWS.gz
+ for details. Highlights include:
+ - notmuch compact command (Closes: #720543).
+ - emacs "tree" view
+ * Remove madduck from uploaders (Closes: #719100).
+
+ -- David Bremner <bremner@debian.org> Mon, 30 Dec 2013 20:28:20 -0400
+
+notmuch (0.17~rc4-1) experimental; urgency=low
+
+ * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org> Sat, 28 Dec 2013 18:30:06 -0400
+
+notmuch (0.17~rc3-1) experimental; urgency=low
+
+ * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org> Sat, 07 Dec 2013 17:05:11 +0800
+
+notmuch (0.17~rc2-1) experimental; urgency=low
+
+ * New upstream release candidate
+ * Remove gdb as build-dep on s390x. This implicitly disables failing
+ atomicity test. For more information, see #728705
+
+ -- David Bremner <bremner@debian.org> Sun, 24 Nov 2013 19:34:28 -0400
+
+notmuch (0.17~rc1-1) experimental; urgency=low
+
+ * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org> Wed, 20 Nov 2013 19:27:48 -0400
+
notmuch (0.16-1~bpo70+1) wheezy-backports; urgency=low
* Rebuild for wheezy-backports.
-- David Bremner <bremner@debian.org> Sun, 01 Sep 2013 19:04:32 -0300
-
notmuch (0.16-1) unstable; urgency=low
* New upstream feature release
ruby, ruby-dev (>>1:1.9.3~),
emacs23-nox | emacs23 (>=23~) | emacs23-lucid (>=23~) |
emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~),
- gdb,
+ gdb [!s390x !ia64],
dtach (>= 0.8)
Standards-Version: 3.9.4
Homepage: http://notmuchmail.org/
Section: mail
Breaks: notmuch (<<0.6~254~)
Replaces: notmuch (<<0.6~254~)
+Conflicts: emacsen-common (<< 2.0.0)
Depends: ${misc:Depends}, notmuch (>= ${source:Version}),
emacs23 (>= 23~) | emacs23-nox (>=23~) | emacs23-lucid (>=23~) |
emacs24 (>= 24~) | emacs24-nox (>=24~) | emacs24-lucid (>=24~)
notmuch (>= 0.4),
libmail-box-perl, libmailtools-perl,
libstring-shellquote-perl, libterm-readline-gnu-perl,
- libfile-which-perl,
${misc:Depends}
-Recommends: mutt, fdupes
+Recommends: mutt
Enhances: notmuch, mutt
Description: thread-based email index, search and tagging (Mutt interface)
notmuch-mutt provides integration among the Mutt mail user agent and
notmuch_database_add_message@Base 0.3
notmuch_database_begin_atomic@Base 0.9~rc1
notmuch_database_close@Base 0.13~rc1
+ notmuch_database_compact@Base 0.17~rc1
notmuch_database_create@Base 0.3
notmuch_database_destroy@Base 0.13~rc1
notmuch_database_end_atomic@Base 0.9~rc1
-
-* This package currently works only with emacs 23. Users of
- pre-release snapshots of emacs 24 can expect problems.
-
* For help in getting started with notmuch and the emacs interface,
see /usr/share/doc/notmuch/README.
- -- David Bremner <bremner@debian.org>, Fri, 1 Jul 2011 11:41:26 -0300
+ -- David Bremner <bremner@debian.org>, Wed, 27 Nov 2013 08:17:11 -0400
FLAVOR=$1
PACKAGE=notmuch
-if [ ${FLAVOR} = emacs ]; then exit 0; fi
-
# We know that the notmuch emacs code doesn't work with emacs before emacs23
if [ ${FLAVOR} = emacs21 ]; then exit 0; fi
if [ ${FLAVOR} = emacs22 ]; then exit 0; fi
FLAVOR=$1
PACKAGE=notmuch
-if [ ${FLAVOR} != emacs ]; then
- echo remove/${PACKAGE}: purging byte-compiled files for ${FLAVOR}
- rm -rf /usr/share/${FLAVOR}/site-lisp/${PACKAGE}
-fi
+echo remove/${PACKAGE}: purging byte-compiled files for ${FLAVOR}
+rm -rf /usr/share/${FLAVOR}/site-lisp/${PACKAGE}
--- /dev/null
+dir="/var/lib/emacsen-common/state/package/installed"
+mkdir -p 0755 ${dir}
+touch ${dir}/notmuch-emacs
+#DEBHELPER#
--- /dev/null
+#DEBHELPER#
+dir="/var/lib/emacsen-common/state/package/installed"
+rm -f ${dir}/notmuch-emacs
usr/share/vim/registry
usr/share/vim/addons/plugin
+usr/share/vim/addons/doc
usr/share/vim/addons/syntax
vim/notmuch.vim usr/share/vim/addons/plugin
+vim/notmuch.txt usr/share/vim/addons/doc
vim/syntax/notmuch-*.vim usr/share/vim/addons/syntax
vim/notmuch.yaml usr/share/vim/registry
--- /dev/null
+single-debian-patch
result.) This may require removing the outer array from the current
"notmuch search --format=json" results.
-Fix '*' to work by simply calling '+' or '-' on a region consisting of
-the entire buffer, (this would avoid one race condition---while still
-leaving other race conditions---but could also potentially make '*' a
-very expensive operation).
-
Add a global keybinding table for notmuch, and then view-specific
tables that add to it.
reached on this issue, and then both reply formats should be updated
to be consistent.
+Return docid-based queries in thread search results, instead of
+message-id-based queries. These are much more compact and much faster
+to later query. This will require support from the library, both for
+retrieving docids (probably as an opaque "get a query that identifies
+this message") and for docid queries.
+
notmuch library
---------------
Add support for custom flag<->tag mappings. In the notmuch
set -o posix
set -o pipefail # bash feature
+readonly DEFAULT_IFS="$IFS" # Note: In this particular case quotes are needed.
+
# Avoid locale-specific differences in output of executed commands
LANG=C LC_ALL=C; export LANG LC_ALL
# Using array here turned out to be unnecessarily complicated
emsgs=''
+emsg_count=0
append_emsg ()
{
+ emsg_count=$((emsg_count + 1))
emsgs="${emsgs:+$emsgs\n} $1"
}
*) verfail "'$VERSION' is a single number" ;;
esac
+echo -n "Checking that LIBNOTMUCH version macros & variables match ... "
+# lib/notmuch.h
+LIBNOTMUCH_MAJOR_VERSION=broken
+LIBNOTMUCH_MINOR_VERSION=broken
+LIBNOTMUCH_MICRO_VERSION=broken
+# lib/Makefile.local
+LIBNOTMUCH_VERSION_MAJOR=borken
+LIBNOTMUCH_VERSION_MINOR=borken
+LIBNOTMUCH_VERSION_RELEASE=borken
+
+eval `awk 'NF == 3 && $1 == "#define" && $2 ~ /^LIBNOTMUCH_[A-Z]+_VERSION$/ \
+ && $3 ~ /^[0-9]+$/ { print $2 "=" $3 }' lib/notmuch.h`
+
+eval `awk 'NF == 3 && $1 ~ /^LIBNOTMUCH_VERSION_[A-Z]+$/ && $2 == "=" \
+ && $3 ~ /^[0-9]+$/ { print $1 "=" $3 }' lib/Makefile.local`
+
+
+check_version_component ()
+{
+ eval local v1=\$LIBNOTMUCH_$1_VERSION
+ eval local v2=\$LIBNOTMUCH_VERSION_$2
+ if [ $v1 != $v2 ]
+ then append_emsg "LIBNOTMUCH_$1_VERSION ($v1) does not equal LIBNOTMUCH_VERSION_$2 ($v2)"
+ fi
+}
+
+old_emsg_count=$emsg_count
+check_version_component MAJOR MAJOR
+check_version_component MINOR MINOR
+check_version_component MICRO RELEASE
+[ $old_emsg_count = $emsg_count ] && echo Yes. || echo No.
echo -n "Checking that this is Debian package for notmuch... "
read deb_notmuch deb_version rest < debian/changelog
colon), e.g. (:id "123" :time 54321 :from "foobar"). Null is printed as
nil, true as t and false as nil.
-This is version 1 of the structured output format.
+This is version 2 of the structured output format.
+
+Version history
+---------------
+
+v1
+- First versioned schema release.
+- Added part.content-length and part.content-transfer-encoding fields.
+
+v2
+- Added the thread_summary.query field.
Common non-terminals
--------------------
---------------------
# --output=summary
-summary = [thread*]
+search_summary = [thread_summary*]
# --output=threads
-threads = [threadid*]
+search_threads = [threadid*]
# --output=messages
-messages = [messageid*]
+search_messages = [messageid*]
# --output=files
-files = [string*]
+search_files = [string*]
# --output=tags
-tags = [string*]
+search_tags = [string*]
-thread = {
+thread_summary = {
thread: threadid,
timestamp: unix_time,
date_relative: string, # user-friendly timestamp
authors: string, # comma-separated names with | between
# matched and unmatched
subject: string,
- tags: [string*]
+ tags: [string*],
+
+ # Two stable query strings identifying exactly the matched and
+ # unmatched messages currently in this thread. The messages
+ # matched by these queries will not change even if more messages
+ # arrive in the thread. If there are no matched or unmatched
+ # messages, the corresponding query will be null (there is no
+ # query that matches nothing). (Added in schema version 2.)
+ query: [string|null, string|null],
}
notmuch reply schema
$(dir)/notmuch.el \
$(dir)/notmuch-query.el \
$(dir)/notmuch-show.el \
+ $(dir)/notmuch-tree.el \
$(dir)/notmuch-wash.el \
$(dir)/notmuch-hello.el \
$(dir)/notmuch-mua.el \
notmuch-hello-query-section
(function :tag "Custom section"))))
+(defcustom notmuch-hello-auto-refresh t
+ "Automatically refresh when returning to the notmuch-hello buffer."
+ :group 'notmuch-hello
+ :type 'boolean)
+
(defvar notmuch-hello-hidden-sections nil
"List of sections titles whose contents are hidden")
search))
(defun notmuch-hello-search (&optional search)
- (interactive)
(unless (null search)
(setq search (notmuch-hello-trim search))
(let ((history-delete-duplicates t))
(add-to-history 'notmuch-search-history search)))
- (notmuch-search search notmuch-search-oldest-first nil nil
- #'notmuch-hello-search-continuation))
+ (notmuch-search search notmuch-search-oldest-first))
(defun notmuch-hello-add-saved-search (widget)
(interactive)
(defun notmuch-hello-widget-search (widget &rest ignore)
(notmuch-search (widget-get widget
:notmuch-search-terms)
- notmuch-search-oldest-first
- nil nil #'notmuch-hello-search-continuation))
+ notmuch-search-oldest-first))
(defun notmuch-saved-search-count (search)
(car (process-lines notmuch-command "count" search)))
(defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png")))
-(defun notmuch-hello-search-continuation()
- (notmuch-hello-update t))
-
(defun notmuch-hello-update (&optional no-display)
"Update the current notmuch view."
;; Lazy - rebuild everything.
- (interactive)
(notmuch-hello no-display))
-(defun notmuch-hello-poll-and-update ()
- "Invoke `notmuch-poll' to import mail, then refresh the current view."
- (interactive)
- (notmuch-poll)
- (notmuch-hello-update))
+(defun notmuch-hello-window-configuration-change ()
+ "Hook function to update the hello buffer when it is switched to."
+ (let ((hello-buf (get-buffer "*notmuch-hello*"))
+ (do-refresh nil))
+ ;; Consider all windows in the currently selected frame, since
+ ;; that's where the configuration change happened. This also
+ ;; refreshes our snapshot of all windows, so we have to do this
+ ;; even if we know we won't refresh (e.g., hello-buf is null).
+ (dolist (window (window-list))
+ (let ((last-buf (window-parameter window 'notmuch-hello-last-buffer))
+ (cur-buf (window-buffer window)))
+ (when (not (eq last-buf cur-buf))
+ ;; This window changed or is new. Update recorded buffer
+ ;; for next time.
+ (set-window-parameter window 'notmuch-hello-last-buffer cur-buf)
+ (when (and (eq cur-buf hello-buf) last-buf)
+ ;; The user just switched to hello in this window (hello
+ ;; is currently visible, was not visible on the last
+ ;; configuration change, and this is not a new window)
+ (setq do-refresh t)))))
+ (when (and do-refresh notmuch-hello-auto-refresh)
+ ;; Refresh hello as soon as we get back to redisplay. On Emacs
+ ;; 24, we can't do it right here because something in this
+ ;; hook's call stack overrides hello's point placement.
+ (run-at-time nil nil #'notmuch-hello t))
+ (when (null hello-buf)
+ ;; Clean up hook
+ (remove-hook 'window-configuration-change-hook
+ #'notmuch-hello-window-configuration-change))))
(defvar notmuch-hello-mode-map
- (let ((map (make-sparse-keymap)))
- (set-keymap-parent map widget-keymap)
+ (let ((map (if (fboundp 'make-composed-keymap)
+ ;; Inherit both widget-keymap and notmuch-common-keymap
+ (make-composed-keymap widget-keymap)
+ ;; Before Emacs 24, keymaps didn't support multiple
+ ;; inheritance,, so just copy the widget keymap since
+ ;; it's unlikely to change.
+ (copy-keymap widget-keymap))))
+ (set-keymap-parent map notmuch-common-keymap)
(define-key map "v" (lambda () "Display the notmuch version" (interactive)
(message "notmuch version %s" (notmuch-version))))
- (define-key map "?" 'notmuch-help)
- (define-key map "q" 'notmuch-kill-this-buffer)
- (define-key map "=" 'notmuch-hello-update)
- (define-key map "G" 'notmuch-hello-poll-and-update)
(define-key map (kbd "<C-tab>") 'widget-backward)
- (define-key map "m" 'notmuch-mua-new-mail)
- (define-key map "s" 'notmuch-hello-search)
map)
"Keymap for \"notmuch hello\" buffers.")
(fset 'notmuch-hello-mode-map notmuch-hello-mode-map)
\\{notmuch-hello-mode-map}"
(interactive)
(kill-all-local-variables)
+ (setq notmuch-buffer-refresh-function #'notmuch-hello-update)
(use-local-map notmuch-hello-mode-map)
(setq major-mode 'notmuch-hello-mode
mode-name "notmuch-hello")
"Run notmuch and display saved searches, known tags, etc."
(interactive)
- (if no-display
- (set-buffer "*notmuch-hello*")
- (switch-to-buffer "*notmuch-hello*"))
+ ;; This may cause a window configuration change, so if the
+ ;; auto-refresh hook is already installed, avoid recursive refresh.
+ (let ((notmuch-hello-auto-refresh nil))
+ (if no-display
+ (set-buffer "*notmuch-hello*")
+ (switch-to-buffer "*notmuch-hello*")))
+
+ ;; Install auto-refresh hook
+ (when notmuch-hello-auto-refresh
+ (add-hook 'window-configuration-change-hook
+ #'notmuch-hello-window-configuration-change))
(let ((target-line (line-number-at-pos))
(target-column (current-column))
:type 'boolean
:group 'notmuch-search)
+(defcustom notmuch-poll-script nil
+ "An external script to incorporate new mail into the notmuch database.
+
+This variable controls the action invoked by
+`notmuch-poll-and-refresh-this-buffer' (bound by default to 'G')
+to incorporate new mail into the notmuch database.
+
+If set to nil (the default), new mail is processed by invoking
+\"notmuch new\". Otherwise, this should be set to a string that
+gives the name of an external script that processes new mail. If
+set to the empty string, no command will be run.
+
+The external script could do any of the following depending on
+the user's needs:
+
+1. Invoke a program to transfer mail to the local mail store
+2. Invoke \"notmuch new\" to incorporate the new mail
+3. Invoke one or more \"notmuch tag\" commands to classify the mail
+
+Note that the recommended way of achieving the same is using
+\"notmuch new\" hooks."
+ :type '(choice (const :tag "notmuch new" nil)
+ (const :tag "Disabled" "")
+ (string :tag "Custom script"))
+ :group 'notmuch-external)
+
;;
(defvar notmuch-search-history nil
:group 'notmuch-search
:group 'notmuch-show)
+(defvar notmuch-common-keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map "?" 'notmuch-help)
+ (define-key map "q" 'notmuch-kill-this-buffer)
+ (define-key map "s" 'notmuch-search)
+ (define-key map "z" 'notmuch-tree)
+ (define-key map "m" 'notmuch-mua-new-mail)
+ (define-key map "=" 'notmuch-refresh-this-buffer)
+ (define-key map "G" 'notmuch-poll-and-refresh-this-buffer)
+ map)
+ "Keymap shared by all notmuch modes.")
+
;; By default clicking on a button does not select the window
;; containing the button (as opposed to clicking on a widget which
;; does). This means that the button action is then executed in the
"Return the user.other_email value (as a list) from the notmuch configuration."
(split-string (notmuch-config-get "user.other_email") "\n"))
+(defun notmuch-poll ()
+ "Run \"notmuch new\" or an external script to import mail.
+
+Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
+depending on the value of `notmuch-poll-script'."
+ (interactive)
+ (if (stringp notmuch-poll-script)
+ (unless (string= notmuch-poll-script "")
+ (call-process notmuch-poll-script nil nil))
+ (call-process notmuch-command nil nil nil "new")))
+
(defun notmuch-kill-this-buffer ()
"Kill the current buffer."
(interactive)
(kill-buffer (current-buffer)))
+(defun notmuch-documentation-first-line (symbol)
+ "Return the first line of the documentation string for SYMBOL."
+ (let ((doc (documentation symbol)))
+ (if doc
+ (with-temp-buffer
+ (insert (documentation symbol t))
+ (goto-char (point-min))
+ (let ((beg (point)))
+ (end-of-line)
+ (buffer-substring beg (point))))
+ "")))
+
+(defun notmuch-prefix-key-description (key)
+ "Given a prefix key code, return a human-readable string representation.
+
+This is basically just `format-kbd-macro' but we also convert ESC to M-."
+ (let ((desc (format-kbd-macro (vector key))))
+ (if (string= desc "ESC")
+ "M-"
+ (concat desc " "))))
+
+
+(defun notmuch-describe-key (actual-key binding prefix ua-keys tail)
+ "Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to TAIL
+
+It does not prepend if ACTUAL-KEY is already listed in TAIL."
+ (let ((key-string (concat prefix (format-kbd-macro actual-key))))
+ ;; We don't include documentation if the key-binding is
+ ;; over-ridden. Note, over-riding a binding automatically hides the
+ ;; prefixed version too.
+ (unless (assoc key-string tail)
+ (when (and ua-keys (symbolp binding)
+ (get binding 'notmuch-prefix-doc))
+ ;; Documentation for prefixed command
+ (let ((ua-desc (key-description ua-keys)))
+ (push (cons (concat ua-desc " " prefix (format-kbd-macro actual-key))
+ (get binding 'notmuch-prefix-doc))
+ tail)))
+ ;; Documentation for command
+ (push (cons key-string
+ (or (and (symbolp binding) (get binding 'notmuch-doc))
+ (notmuch-documentation-first-line binding)))
+ tail)))
+ tail)
+
+(defun notmuch-describe-remaps (remap-keymap ua-keys base-keymap prefix tail)
+ ;; Remappings are represented as a binding whose first "event" is
+ ;; 'remap. Hence, if the keymap has any remappings, it will have a
+ ;; binding whose "key" is 'remap, and whose "binding" is itself a
+ ;; keymap that maps not from keys to commands, but from old (remapped)
+ ;; functions to the commands to use in their stead.
+ (map-keymap
+ (lambda (command binding)
+ (mapc
+ (lambda (actual-key)
+ (setq tail (notmuch-describe-key actual-key binding prefix ua-keys tail)))
+ (where-is-internal command base-keymap)))
+ remap-keymap)
+ tail)
+
+(defun notmuch-describe-keymap (keymap ua-keys base-keymap &optional prefix tail)
+ "Return a list of cons cells, each describing one binding in KEYMAP.
+
+Each cons cell consists of a string giving a human-readable
+description of the key, and a one-line description of the bound
+function. See `notmuch-help' for an overview of how this
+documentation is extracted.
+
+UA-KEYS should be a key sequence bound to `universal-argument'.
+It will be used to describe bindings of commands that support a
+prefix argument. PREFIX and TAIL are used internally."
+ (map-keymap
+ (lambda (key binding)
+ (cond ((mouse-event-p key) nil)
+ ((keymapp binding)
+ (setq tail
+ (if (eq key 'remap)
+ (notmuch-describe-remaps
+ binding ua-keys base-keymap prefix tail)
+ (notmuch-describe-keymap
+ binding ua-keys base-keymap (notmuch-prefix-key-description key) tail))))
+ (binding
+ (setq tail (notmuch-describe-key (vector key) binding prefix ua-keys tail)))))
+ keymap)
+ tail)
+
+(defun notmuch-substitute-command-keys (doc)
+ "Like `substitute-command-keys' but with documentation, not function names."
+ (let ((beg 0))
+ (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
+ (let ((desc
+ (save-match-data
+ (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
+ (keymap (symbol-value (intern keymap-name)))
+ (ua-keys (where-is-internal 'universal-argument keymap t))
+ (desc-alist (notmuch-describe-keymap keymap ua-keys keymap))
+ (desc-list (mapcar (lambda (arg) (concat (car arg) "\t" (cdr arg))) desc-alist)))
+ (mapconcat #'identity desc-list "\n")))))
+ (setq doc (replace-match desc 1 1 doc)))
+ (setq beg (match-end 0)))
+ doc))
+
+(defun notmuch-help ()
+ "Display help for the current notmuch mode.
+
+This is similar to `describe-function' for the current major
+mode, but bindings tables are shown with documentation strings
+rather than command names. By default, this uses the first line
+of each command's documentation string. A command can override
+this by setting the 'notmuch-doc property of its command symbol.
+A command that supports a prefix argument can explicitly document
+its prefixed behavior by setting the 'notmuch-prefix-doc property
+of its command symbol."
+ (interactive)
+ (let* ((mode major-mode)
+ (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
+ (with-current-buffer (generate-new-buffer "*notmuch-help*")
+ (insert doc)
+ (goto-char (point-min))
+ (set-buffer-modified-p nil)
+ (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
+
+(defvar notmuch-buffer-refresh-function nil
+ "Function to call to refresh the current buffer.")
+(make-variable-buffer-local 'notmuch-buffer-refresh-function)
+
+(defun notmuch-refresh-this-buffer ()
+ "Refresh the current buffer."
+ (interactive)
+ (when notmuch-buffer-refresh-function
+ (if (commandp notmuch-buffer-refresh-function)
+ ;; Pass prefix argument, etc.
+ (call-interactively notmuch-buffer-refresh-function)
+ (funcall notmuch-buffer-refresh-function))))
+
+(defun notmuch-poll-and-refresh-this-buffer ()
+ "Invoke `notmuch-poll' to import mail, then refresh the current buffer."
+ (interactive)
+ (notmuch-poll)
+ (notmuch-refresh-this-buffer))
+
(defun notmuch-prettify-subject (subject)
;; This function is used by `notmuch-search-process-filter' which
;; requires that we not disrupt its' matching state.
"[No Subject]"
subject)))
+(defun notmuch-sanitize (str)
+ "Sanitize control character in STR.
+
+This includes newlines, tabs, and other funny characters."
+ (replace-regexp-in-string "[[:cntrl:]\x7f\u2028\u2029]+" " " str))
+
(defun notmuch-escape-boolean-term (term)
"Escape a boolean term for use in a query.
"Return a query that matches the message with id ID."
(concat "id:" (notmuch-escape-boolean-term id)))
+(defun notmuch-hex-encode (str)
+ "Hex-encode STR (e.g., as used by batch tagging).
+
+This replaces spaces, percents, and double quotes in STR with
+%NN where NN is the hexadecimal value of the character."
+ (replace-regexp-in-string
+ "[ %\"]" (lambda (match) (format "%%%02x" (aref match 0))) str))
+
;;
(defun notmuch-common-do-stash (text)
;; `notmuch-logged-error' does not return.
))))
+(defun notmuch-call-notmuch--helper (destination args)
+ "Helper for synchronous notmuch invocation commands.
+
+This wraps `call-process'. DESTINATION has the same meaning as
+for `call-process'. ARGS is as described for
+`notmuch-call-notmuch-process'."
+
+ (let (stdin-string)
+ (while (keywordp (car args))
+ (case (car args)
+ (:stdin-string (setq stdin-string (cadr args)
+ args (cddr args)))
+ (otherwise
+ (error "Unknown keyword argument: %s" (car args)))))
+ (if (null stdin-string)
+ (apply #'call-process notmuch-command nil destination nil args)
+ (insert stdin-string)
+ (apply #'call-process-region (point-min) (point-max)
+ notmuch-command t destination nil args))))
+
+(defun notmuch-call-notmuch-process (&rest args)
+ "Synchronously invoke `notmuch-command' with ARGS.
+
+The caller may provide keyword arguments before ARGS. Currently
+supported keyword arguments are:
+
+ :stdin-string STRING - Write STRING to stdin
+
+If notmuch exits with a non-zero status, output from the process
+will appear in a buffer named \"*Notmuch errors*\" and an error
+will be signaled."
+ (with-temp-buffer
+ (let ((status (notmuch-call-notmuch--helper t args)))
+ (notmuch-check-exit-status status (cons notmuch-command args)
+ (buffer-string)))))
+
(defun notmuch-call-notmuch-sexp (&rest args)
"Invoke `notmuch-command' with ARGS and return the parsed S-exp output.
-If notmuch exits with a non-zero status, this will pop up a
-buffer containing notmuch's output and signal an error."
+This is equivalent to `notmuch-call-notmuch-process', but parses
+notmuch's output as an S-expression and returns the parsed value.
+Like `notmuch-call-notmuch-process', if notmuch exits with a
+non-zero status, this will report its output and signal an
+error."
(with-temp-buffer
(let ((err-file (make-temp-file "nmerr")))
(unwind-protect
- (let ((status (apply #'call-process
- notmuch-command nil (list t err-file) nil args)))
+ (let ((status (notmuch-call-notmuch--helper (list t err-file) args)))
(notmuch-check-exit-status status (cons notmuch-command args)
(buffer-string) err-file)
(goto-char (point-min))
(defvar notmuch-show-process-crypto nil)
(make-variable-buffer-local 'notmuch-show-process-crypto)
-
(provide 'notmuch-lib)
;; Local Variables:
(eval-when-compile (require 'cl))
+(declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth &optional hide))
+
;;
(defcustom notmuch-mua-send-hook '(notmuch-mua-message-send-hook)
collect part))
(defun notmuch-mua-insert-quotable-part (message part)
- (save-restriction
- (narrow-to-region (point) (point))
- (notmuch-mm-display-part-inline message part (plist-get part :id)
- (plist-get part :content-type)
- notmuch-show-process-crypto)
- (goto-char (point-max))))
+ ;; We don't want text properties leaking from the show renderer into
+ ;; the reply so we use a temp buffer. Also we don't want hooks, such
+ ;; as notmuch-wash-*, to be run on the quotable part so we set
+ ;; notmuch-show-insert-text/plain-hook to nil.
+ (insert (with-temp-buffer
+ (let ((notmuch-show-insert-text/plain-hook nil))
+ ;; Show the part but do not add buttons.
+ (notmuch-show-insert-bodypart message part 0 'no-buttons))
+ (buffer-substring-no-properties (point-min) (point-max)))))
;; There is a bug in emacs 23's message.el that results in a newline
;; not being inserted after the References header, so the next header
nil (notmuch-mua-get-switch-function))))
;; Insert the message body - but put it in front of the signature
- ;; if one is present
- (goto-char (point-max))
- (if (re-search-backward message-signature-separator nil t)
- (forward-line -1)
- (goto-char (point-max)))
+ ;; if one is present, and after any other content
+ ;; message*setup-hooks may have added to the message body already.
+ (save-restriction
+ (message-goto-body)
+ (narrow-to-region (point) (point-max))
+ (goto-char (point-max))
+ (if (re-search-backward message-signature-separator nil t)
+ (if message-signature-insert-empty-line
+ (forward-line -1))
+ (goto-char (point-max))))
(let ((from (plist-get original-headers :From))
(date (plist-get original-headers :Date))
(ido-completing-read "Send mail From: " notmuch-identities
nil nil nil 'notmuch-mua-sender-history (car notmuch-identities)))))
+(put 'notmuch-mua-new-mail 'notmuch-prefix-doc "... and prompt for sender")
(defun notmuch-mua-new-mail (&optional prompt-for-sender)
- "Invoke the notmuch mail composition window.
+ "Compose new mail.
If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
the From: address first."
(defun notmuch-mua-new-forward-message (&optional prompt-for-sender)
"Invoke the notmuch message forwarding window.
+The current buffer must contain an RFC2822 message to forward.
+
If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
the From: address first."
- (interactive "P")
(if (or prompt-for-sender notmuch-always-prompt-for-sender)
(let* ((sender (notmuch-mua-prompt-for-sender))
(address-components (mail-extract-address-components sender))
(notmuch-mua-forward-message)))
(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all)
- "Invoke the notmuch reply window."
- (interactive "P")
+ "Compose a reply to the message identified by QUERY-STRING.
+
+If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
+the From: address first. If REPLY-ALL is non-nil, the message
+will be addressed to all recipients of the source message."
+
+;; In current emacs (24.3) select-active-regions is set to t by
+;; default. The reply insertion code sets the region to the quoted
+;; message to make it easy to delete (kill-region or C-w). These two
+;; things combine to put the quoted message in the primary selection.
+;;
+;; This is not what the user wanted and is a privacy risk (accidental
+;; pasting of the quoted message). We can avoid some of the problems
+;; by let-binding select-active-regions to nil. This fixes if the
+;; primary selection was previously in a non-emacs window but not if
+;; it was in an emacs window. To avoid the problem in the latter case
+;; we deactivate mark.
+
(let ((sender
(when prompt-for-sender
- (notmuch-mua-prompt-for-sender))))
- (notmuch-mua-reply query-string sender reply-all)))
+ (notmuch-mua-prompt-for-sender)))
+ (select-active-regions nil))
+ (notmuch-mua-reply query-string sender reply-all)
+ (deactivate-mark)))
(defun notmuch-mua-send-and-exit (&optional arg)
(interactive "P")
(declare-function notmuch-search-next-thread "notmuch" nil)
(declare-function notmuch-search-previous-thread "notmuch" nil)
(declare-function notmuch-search-show-thread "notmuch" nil)
+(declare-function notmuch-foreach-mime-part "notmuch" (function mm-handle))
+(declare-function notmuch-count-attachments "notmuch" (mm-handle))
+(declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp))
+(declare-function notmuch-tree "notmuch-tree"
+ (&optional query query-context target buffer-name open-target))
(defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
"Headers that should be shown in a message, in this order.
(make-variable-buffer-local 'notmuch-show-indent-content)
(put 'notmuch-show-indent-content 'permanent-local t)
+(defvar notmuch-show-attachment-debug nil
+ "If t log stdout and stderr from attachment handlers
+
+When set to nil (the default) stdout and stderr from attachment
+handlers is discarded. When set to t the stdout and stderr from
+each attachment handler is logged in buffers with names beginning
+\" *notmuch-part*\". This option requires emacs version at least
+24.3 to work.")
+
(defcustom notmuch-show-stash-mlarchive-link-alist
'(("Gmane" . "http://mid.gmane.org/")
("MARC" . "http://marc.info/?i=")
)))
(mm-display-parts (mm-dissect-buffer)))))
-(defun notmuch-foreach-mime-part (function mm-handle)
- (cond ((stringp (car mm-handle))
- (dolist (part (cdr mm-handle))
- (notmuch-foreach-mime-part function part)))
- ((bufferp (car mm-handle))
- (funcall function mm-handle))
- (t (dolist (part mm-handle)
- (notmuch-foreach-mime-part function part)))))
-
-(defun notmuch-count-attachments (mm-handle)
- (let ((count 0))
- (notmuch-foreach-mime-part
- (lambda (p)
- (let ((disposition (mm-handle-disposition p)))
- (and (listp disposition)
- (or (equal (car disposition) "attachment")
- (and (equal (car disposition) "inline")
- (assq 'filename disposition)))
- (incf count))))
- mm-handle)
- count))
-
-(defun notmuch-save-attachments (mm-handle &optional queryp)
- (notmuch-foreach-mime-part
- (lambda (p)
- (let ((disposition (mm-handle-disposition p)))
- (and (listp disposition)
- (or (equal (car disposition) "attachment")
- (and (equal (car disposition) "inline")
- (assq 'filename disposition)))
- (or (not queryp)
- (y-or-n-p
- (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
- (mm-save-part p))))
- mm-handle))
-
(defun notmuch-show-save-attachments ()
"Save all attachments from the current message."
(interactive)
message at DEPTH in the current thread."
(let ((start (point)))
(insert (notmuch-show-spaces-n (* notmuch-show-indent-messages-width depth))
- (notmuch-show-clean-address (plist-get headers :From))
+ (notmuch-sanitize
+ (notmuch-show-clean-address (plist-get headers :From)))
" ("
date
") ("
(defun notmuch-show-insert-header (header header-value)
"Insert a single header."
- (insert header ": " header-value "\n"))
+ (insert header ": " (notmuch-sanitize header-value) "\n"))
(defun notmuch-show-insert-headers (headers)
"Insert the headers of the current message."
(new-start (button-start button))
(button-label (button-get button :base-label))
(old-point (point))
- (properties (text-properties-at (point)))
+ (properties (text-properties-at (button-start button)))
(inhibit-read-only t))
;; Toggle the button itself.
(button-put button :notmuch-part-hidden (not show))
;; Render the primary part.
(notmuch-show-insert-bodypart msg (car inner-parts) depth)
+ ;; Add hidden buttons for the rest
+ (mapc (lambda (inner-part)
+ (notmuch-show-insert-bodypart msg inner-part depth t))
+ (cdr inner-parts))
(when notmuch-show-indent-multipart
(indent-rigidly start (point) 1)))
(defun notmuch-show-insert-bodypart (msg part depth &optional hide)
"Insert the body part PART at depth DEPTH in the current thread.
-If HIDE is non-nil then initially hide this part."
+HIDE determines whether to show or hide the part and the button
+as follows: If HIDE is nil, show the part and the button. If HIDE
+is t, hide the part initially and show the button. If HIDE is
+'no-buttons, show the part but do not add any buttons (this is
+useful for quoting in replies)."
(let* ((content-type (downcase (plist-get part :content-type)))
(mime-type (or (and (string= content-type "application/octet-stream")
content-type))
(nth (plist-get part :id))
(beg (point))
- ;; We omit the part button for the first (or only) part if this is text/plain.
- (button (unless (and (string= mime-type "text/plain") (<= nth 1))
+ ;; Hide the part initially if HIDE is t.
+ (show-part (not (equal hide t)))
+ ;; We omit the part button for the first (or only) part if
+ ;; this is text/plain, or HIDE is 'no-buttons.
+ (button (unless (or (equal hide 'no-buttons)
+ (and (string= mime-type "text/plain") (<= nth 1)))
(notmuch-show-insert-part-header nth mime-type content-type (plist-get part :filename))))
(content-beg (point)))
;; Store the computed mime-type for later use (e.g. by attachment handlers).
(plist-put part :computed-type mime-type)
- (if (not hide)
+ (if show-part
(notmuch-show-insert-bodypart-internal msg part mime-type nth depth button)
(button-put button :notmuch-lazy-part
(list msg part mime-type nth depth button)))
(insert "\n"))
;; We do not create the overlay for hidden (lazy) parts until
;; they are inserted.
- (if (not hide)
+ (if show-part
(notmuch-show-create-part-overlays button content-beg (point))
(save-excursion
(notmuch-show-toggle-part-invisibility button)))
(make-text-button (first link) (second link)
:type 'notmuch-button-type
'action `(lambda (arg)
- (notmuch-show ,(third link)))
+ (notmuch-show ,(third link) current-prefix-arg))
'follow-link t
'help-echo "Mouse-1, RET: search for this message"
'face goto-address-mail-face)))))
;;;###autoload
-(defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name)
+(defun notmuch-show (thread-id &optional elide-toggle parent-buffer query-context buffer-name)
"Run \"notmuch show\" with the given thread ID and display results.
+ELIDE-TOGGLE, if non-nil, inverts the default elide behavior.
+
The optional PARENT-BUFFER is the notmuch-search buffer from
which this notmuch-show command was executed, (so that the
next thread from that buffer can be show when done with this
which the message thread is shown. If it is nil (which occurs
when the command is called interactively) the argument to the
function is used."
- (interactive "sNotmuch show: ")
+ (interactive "sNotmuch show: \nP")
(let ((buffer-name (generate-new-buffer-name
(or buffer-name
(concat "*notmuch-" thread-id "*")))))
(setq notmuch-show-process-crypto notmuch-crypto-process-mime)
;; Set the default value for
;; `notmuch-show-elide-non-matching-messages' in this buffer. If
- ;; there is a prefix argument, invert the default.
+ ;; elide-toggle is set, invert the default.
(setq notmuch-show-elide-non-matching-messages notmuch-show-only-matching-messages)
- (if current-prefix-arg
+ (if elide-toggle
(setq notmuch-show-elide-non-matching-messages (not notmuch-show-elide-non-matching-messages)))
(setq notmuch-show-thread-id thread-id
(jit-lock-register #'notmuch-show-buttonise-links)
;; Set the header line to the subject of the first message.
- (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))
+ (setq header-line-format (notmuch-sanitize (notmuch-show-strip-re (notmuch-show-get-subject))))
(run-hooks 'notmuch-show-hook))))
(defvar notmuch-show-mode-map
(let ((map (make-sparse-keymap)))
- (define-key map "?" 'notmuch-help)
- (define-key map "q" 'notmuch-kill-this-buffer)
+ (set-keymap-parent map notmuch-common-keymap)
+ (define-key map "Z" 'notmuch-tree-from-show-current-query)
(define-key map (kbd "<C-tab>") 'widget-backward)
(define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
(define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
(define-key map (kbd "TAB") 'notmuch-show-next-button)
- (define-key map "s" 'notmuch-search)
- (define-key map "m" 'notmuch-mua-new-mail)
(define-key map "f" 'notmuch-show-forward-message)
(define-key map "r" 'notmuch-show-reply-sender)
(define-key map "R" 'notmuch-show-reply)
(define-key map "w" 'notmuch-show-save-attachments)
(define-key map "V" 'notmuch-show-view-raw-message)
(define-key map "c" 'notmuch-show-stash-map)
- (define-key map "=" 'notmuch-show-refresh-view)
(define-key map "h" 'notmuch-show-toggle-visibility-headers)
(define-key map "*" 'notmuch-show-tag-all)
(define-key map "-" 'notmuch-show-remove-tag)
\\{notmuch-show-mode-map}"
(interactive)
(kill-all-local-variables)
+ (setq notmuch-buffer-refresh-function #'notmuch-show-refresh-view)
(use-local-map notmuch-show-mode-map)
(setq major-mode 'notmuch-show-mode
mode-name "notmuch-show")
(setq buffer-read-only t
truncate-lines t))
+(defun notmuch-tree-from-show-current-query ()
+ "Call notmuch tree with the current query"
+ (interactive)
+ (notmuch-tree notmuch-show-thread-id
+ notmuch-show-query-context
+ (notmuch-show-get-message-id)))
+
(defun notmuch-show-move-to-message-top ()
(goto-char (notmuch-show-message-top)))
"Are the headers of the current message visible?"
(notmuch-show-get-prop :headers-visible))
+(put 'notmuch-show-mark-read 'notmuch-prefix-doc
+ "Mark the current message as unread.")
(defun notmuch-show-mark-read (&optional unread)
"Mark the current message as read.
(notmuch-show-archive-thread-then-next)))
(defun notmuch-show-rewind ()
- "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
+ "Backup through the thread (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
Specifically, if the beginning of the previous email is fewer
than `window-height' lines from the current point, move to it
;; Move to the previous message.
(notmuch-show-previous-message)))))
+(put 'notmuch-show-reply 'notmuch-prefix-doc "... and prompt for sender")
(defun notmuch-show-reply (&optional prompt-for-sender)
"Reply to the sender and all recipients of the current message."
(interactive "P")
(notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender t))
+(put 'notmuch-show-reply-sender 'notmuch-prefix-doc "... and prompt for sender")
(defun notmuch-show-reply-sender (&optional prompt-for-sender)
"Reply to the sender of the current message."
(interactive "P")
(notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender nil))
+(put 'notmuch-show-forward-message 'notmuch-prefix-doc
+ "... and prompt for sender")
(defun notmuch-show-forward-message (&optional prompt-for-sender)
"Forward the current message."
(interactive "P")
(set-buffer-modified-p nil)
(view-buffer buf 'kill-buffer-if-not-modified)))
+(put 'notmuch-show-pipe-message 'notmuch-doc
+ "Pipe the contents of the current message to a command.")
+(put 'notmuch-show-pipe-message 'notmuch-prefix-doc
+ "Pipe the thread as an mbox to a command.")
(defun notmuch-show-pipe-message (entire-thread command)
- "Pipe the contents of the current message (or thread) to the given command.
+ "Pipe the contents of the current message (or thread) to COMMAND.
-The given command will be executed with the raw contents of the
-current email message as stdin. Anything printed by the command
-to stdout or stderr will appear in the *notmuch-pipe* buffer.
+COMMAND will be executed with the raw contents of the current
+email message as stdin. Anything printed by the command to stdout
+or stderr will appear in the *notmuch-pipe* buffer.
-When invoked with a prefix argument, the command will receive all
-open messages in the current thread (formatted as an mbox) rather
-than only the current message."
+If ENTIRE-THREAD is non-nil (or when invoked with a prefix
+argument), COMMAND will receive all open messages in the current
+thread (formatted as an mbox) rather than only the current
+message."
(interactive (let ((query-string (if current-prefix-arg
"Pipe all open messages to command: "
"Pipe message to command: ")))
(notmuch-tag (notmuch-show-get-message-id) tag-changes)
(notmuch-show-set-tags new-tags))))
-(defun notmuch-show-tag (&optional tag-changes)
+(defun notmuch-show-tag (tag-changes)
"Change tags for the current message.
See `notmuch-tag' for information on the format of TAG-CHANGES."
- (interactive)
- (let* ((tag-changes (notmuch-tag (notmuch-show-get-message-id) tag-changes))
- (current-tags (notmuch-show-get-tags))
+ (interactive (list (notmuch-read-tag-changes (notmuch-show-get-tags)
+ "Tag message")))
+ (notmuch-tag (notmuch-show-get-message-id) tag-changes)
+ (let* ((current-tags (notmuch-show-get-tags))
(new-tags (notmuch-update-tags current-tags tag-changes)))
(unless (equal current-tags new-tags)
(notmuch-show-set-tags new-tags))))
-(defun notmuch-show-tag-all (&optional tag-changes)
+(defun notmuch-show-tag-all (tag-changes)
"Change tags for all messages in the current show buffer.
See `notmuch-tag' for information on the format of TAG-CHANGES."
- (interactive)
- (setq tag-changes (notmuch-tag (notmuch-show-get-messages-ids-search) tag-changes))
+ (interactive
+ (list (let (tags)
+ (notmuch-show-mapc
+ (lambda () (setq tags (append (notmuch-show-get-tags) tags))))
+ (notmuch-read-tag-changes tags "Tag thread"))))
+ (notmuch-tag (notmuch-show-get-messages-ids-search) tag-changes)
(notmuch-show-mapc
(lambda ()
(let* ((current-tags (notmuch-show-get-tags))
(unless (equal current-tags new-tags)
(notmuch-show-set-tags new-tags))))))
-(defun notmuch-show-add-tag ()
- "Same as `notmuch-show-tag' but sets initial input to '+'."
- (interactive)
- (notmuch-show-tag "+"))
+(defun notmuch-show-add-tag (tag-changes)
+ "Change tags for the current message (defaulting to add).
-(defun notmuch-show-remove-tag ()
- "Same as `notmuch-show-tag' but sets initial input to '-'."
- (interactive)
- (notmuch-show-tag "-"))
+Same as `notmuch-show-tag' but sets initial input to '+'."
+ (interactive
+ (list (notmuch-read-tag-changes (notmuch-show-get-tags) "Tag message" "+")))
+ (notmuch-show-tag tag-changes))
+
+(defun notmuch-show-remove-tag (tag-changes)
+ "Change tags for the current message (defaulting to remove).
+
+Same as `notmuch-show-tag' but sets initial input to '-'."
+ (interactive
+ (list (notmuch-read-tag-changes (notmuch-show-get-tags) "Tag message" "-")))
+ (notmuch-show-tag tag-changes))
(defun notmuch-show-toggle-visibility-headers ()
"Toggle the visibility of the current message headers."
(not (plist-get props :message-visible))))
(force-window-update))
+(put 'notmuch-show-open-or-close-all 'notmuch-doc "Show all messages.")
+(put 'notmuch-show-open-or-close-all 'notmuch-prefix-doc "Hide all messages.")
(defun notmuch-show-open-or-close-all ()
"Set the visibility all of the messages in the current thread.
+
By default make all of the messages visible. With a prefix
argument, hide all of the messages."
(interactive)
(interactive)
(notmuch-show-next-thread t t))
+(put 'notmuch-show-archive-thread 'notmuch-prefix-doc
+ "Un-archive each message in thread.")
(defun notmuch-show-archive-thread (&optional unarchive)
"Archive each message in thread.
(notmuch-show-archive-thread)
(notmuch-show-next-thread))
+(put 'notmuch-show-archive-message 'notmuch-prefix-doc
+ "Un-archive the current message.")
(defun notmuch-show-archive-message (&optional unarchive)
"Archive the current message.
(interactive)
(notmuch-common-do-stash (notmuch-show-get-from)))
+(put 'notmuch-show-stash-message-id 'notmuch-prefix-doc
+ "Copy thread: query matching current thread to kill-ring.")
(defun notmuch-show-stash-message-id (&optional stash-thread-id)
"Copy id: query matching the current message to kill-ring.
This ensures that the temporary buffer created for the mm-handle
is destroyed when FN returns."
(let ((handle (notmuch-show-current-part-handle)))
+ ;; emacs 24.3+ puts stdout/stderr into the calling buffer so we
+ ;; call it from a temp-buffer, unless
+ ;; notmuch-show-attachment-debug is non-nil in which case we put
+ ;; it in " *notmuch-part*".
(unwind-protect
- (funcall fn handle)
+ (if notmuch-show-attachment-debug
+ (with-current-buffer (generate-new-buffer " *notmuch-part*")
+ (funcall fn handle))
+ (with-temp-buffer
+ (funcall fn handle)))
(kill-buffer (mm-handle-buffer handle)))))
(defun notmuch-show-part-button-default (&optional button)
(defcustom notmuch-tag-formats
'(("unread" (propertize tag 'face '(:foreground "red")))
- ("flagged" (notmuch-tag-format-image-data tag (notmuch-tag-star-icon))))
+ ("flagged" (propertize tag 'face '(:foreground "blue"))
+ (notmuch-tag-format-image-data tag (notmuch-tag-star-icon))))
"Custom formats for individual tags.
This gives a list that maps from tag names to lists of formatting
"Variable to store minibuffer history for
`notmuch-read-tag-changes' function.")
-(defun notmuch-tag-completions (&optional search-terms)
+(defun notmuch-tag-completions (&rest search-terms)
+ "Return a list of tags for messages matching SEARCH-TERMS.
+
+Returns all tags if no search terms are given."
(if (null search-terms)
(setq search-terms (list "*")))
(split-string
"\n+" t))
(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
- (let ((tag-list (notmuch-tag-completions search-terms)))
+ (let ((tag-list (apply #'notmuch-tag-completions search-terms)))
(completing-read prompt tag-list nil nil nil 'notmuch-select-tag-history)))
-(defun notmuch-read-tag-changes (&optional initial-input &rest search-terms)
+(defun notmuch-read-tag-changes (current-tags &optional prompt initial-input)
+ "Prompt for tag changes in the minibuffer.
+
+CURRENT-TAGS is a list of tags that are present on the message or
+messages to be changed. These are offered as tag removal
+completions. CURRENT-TAGS may contain duplicates. PROMPT, if
+non-nil, is the query string to present in the minibuffer. It
+defaults to \"Tags\". INITIAL-INPUT, if non-nil, will be the
+initial input in the minibuffer."
+
(let* ((all-tag-list (notmuch-tag-completions))
(add-tag-list (mapcar (apply-partially 'concat "+") all-tag-list))
- (remove-tag-list (mapcar (apply-partially 'concat "-")
- (if (null search-terms)
- all-tag-list
- (notmuch-tag-completions search-terms))))
+ (remove-tag-list (mapcar (apply-partially 'concat "-") current-tags))
(tag-list (append add-tag-list remove-tag-list))
+ (prompt (concat (or prompt "Tags") " (+add -drop): "))
(crm-separator " ")
;; By default, space is bound to "complete word" function.
;; Re-bind it to insert a space instead. Note that <tab>
(set-keymap-parent map crm-local-completion-map)
(define-key map " " 'self-insert-command)
map)))
- (delete "" (completing-read-multiple "Tags (+add -drop): "
- tag-list nil nil initial-input
+ (delete "" (completing-read-multiple
+ prompt
+ ;; Append the separator to each completion so when the
+ ;; user completes a tag they can immediately begin
+ ;; entering another. `completing-read-multiple'
+ ;; ultimately splits the input on crm-separator, so we
+ ;; don't need to strip this back off (we just need to
+ ;; delete "empty" entries caused by trailing spaces).
+ (mapcar (lambda (tag-op) (concat tag-op crm-separator)) tag-list)
+ nil nil initial-input
'notmuch-read-tag-changes-history))))
(defun notmuch-update-tags (tags tag-changes)
(error "Changed tag must be of the form `+this_tag' or `-that_tag'")))))
(sort result-tags 'string<)))
-(defun notmuch-tag (query &optional tag-changes)
+(defconst notmuch-tag-argument-limit 1000
+ "Use batch tagging if the tagging query is longer than this.
+
+This limits the length of arguments passed to the notmuch CLI to
+avoid system argument length limits and performance problems.")
+
+(defun notmuch-tag (query tag-changes)
"Add/remove tags in TAG-CHANGES to messages matching QUERY.
QUERY should be a string containing the search-terms.
-TAG-CHANGES can take multiple forms. If TAG-CHANGES is a list of
-strings of the form \"+tag\" or \"-tag\" then those are the tag
-changes applied. If TAG-CHANGES is a string then it is
-interpreted as a single tag change. If TAG-CHANGES is the string
-\"-\" or \"+\", or null, then the user is prompted to enter the
-tag changes.
+TAG-CHANGES is a list of strings of the form \"+tag\" or
+\"-tag\" to add or remove tags, respectively.
Note: Other code should always use this function alter tags of
messages instead of running (notmuch-call-notmuch-process \"tag\" ..)
directly, so that hooks specified in notmuch-before-tag-hook and
notmuch-after-tag-hook will be run."
;; Perform some validation
- (if (string-or-null-p tag-changes)
- (if (or (string= tag-changes "-") (string= tag-changes "+") (null tag-changes))
- (setq tag-changes (notmuch-read-tag-changes tag-changes query))
- (setq tag-changes (list tag-changes))))
(mapc (lambda (tag-change)
(unless (string-match-p "^[-+]\\S-+$" tag-change)
(error "Tag must be of the form `+this_tag' or `-that_tag'")))
tag-changes)
(unless (null tag-changes)
(run-hooks 'notmuch-before-tag-hook)
- (apply 'notmuch-call-notmuch-process "tag"
- (append tag-changes (list "--" query)))
- (run-hooks 'notmuch-after-tag-hook))
- ;; in all cases we return tag-changes as a list
- tag-changes)
+ (if (<= (length query) notmuch-tag-argument-limit)
+ (apply 'notmuch-call-notmuch-process "tag"
+ (append tag-changes (list "--" query)))
+ ;; Use batch tag mode to avoid argument length limitations
+ (let ((batch-op (concat (mapconcat #'notmuch-hex-encode tag-changes " ")
+ " -- " query)))
+ (notmuch-call-notmuch-process :stdin-string batch-op "tag" "--batch")))
+ (run-hooks 'notmuch-after-tag-hook)))
(defun notmuch-tag-change-list (tags &optional reverse)
"Convert TAGS into a list of tag changes.
--- /dev/null
+;; notmuch-tree.el --- displaying notmuch forests.
+;;
+;; Copyright © Carl Worth
+;; Copyright © David Edmondson
+;; Copyright © Mark Walters
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+;; Mark Walters <markwalters1009@gmail.com>
+
+(require 'mail-parse)
+
+(require 'notmuch-lib)
+(require 'notmuch-query)
+(require 'notmuch-show)
+(require 'notmuch-tag)
+(require 'notmuch-parser)
+
+(eval-when-compile (require 'cl))
+(declare-function notmuch-search "notmuch" (&optional query oldest-first target-thread target-line))
+(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
+(declare-function notmuch-read-query "notmuch" (prompt))
+(declare-function notmuch-search-find-thread-id "notmuch" (&optional bare))
+(declare-function notmuch-search-find-subject "notmuch" ())
+
+;; the following variable is defined in notmuch.el
+(defvar notmuch-search-query-string)
+
+(defgroup notmuch-tree nil
+ "Showing message and thread structure."
+ :group 'notmuch)
+
+(defcustom notmuch-tree-show-out nil
+ "View selected messages in new window rather than split-pane."
+ :type 'boolean
+ :group 'notmuch-tree)
+
+(defcustom notmuch-tree-result-format
+ `(("date" . "%12s ")
+ ("authors" . "%-20s")
+ ((("tree" . "%s")("subject" . "%s")) ." %-54s ")
+ ("tags" . "(%s)"))
+ "Result formatting for Tree view. Supported fields are: date,
+ authors, subject, tree, tags. Tree means the thread tree
+ box graphics. The field may also be a list in which case
+ the formatting rules are applied recursively and then the
+ output of all the fields in the list is inserted
+ according to format-string.
+
+Note the author string should not contain
+ whitespace (put it in the neighbouring fields instead).
+ For example:
+ (setq notmuch-tree-result-format \(\(\"authors\" . \"%-40s\"\)
+ \(\"subject\" . \"%s\"\)\)\)"
+ :type '(alist :key-type (string) :value-type (string))
+ :group 'notmuch-tree)
+
+;; Faces for messages that match the query.
+(defface notmuch-tree-match-date-face
+ '((t :inherit default))
+ "Face used in tree mode for the date in messages matching the query."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-match-author-face
+ '((((class color)
+ (background dark))
+ (:foreground "OliveDrab1"))
+ (((class color)
+ (background light))
+ (:foreground "dark blue"))
+ (t
+ (:bold t)))
+ "Face used in tree mode for the date in messages matching the query."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-match-subject-face
+ '((t :inherit default))
+ "Face used in tree mode for the subject in messages matching the query."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-match-tree-face
+ '((t :inherit default))
+ "Face used in tree mode for the thread tree block graphics in messages matching the query."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-match-tag-face
+ '((((class color)
+ (background dark))
+ (:foreground "OliveDrab1"))
+ (((class color)
+ (background light))
+ (:foreground "navy blue" :bold t))
+ (t
+ (:bold t)))
+ "Face used in tree mode for tags in messages matching the query."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+;; Faces for messages that do not match the query.
+(defface notmuch-tree-no-match-date-face
+ '((t (:foreground "gray")))
+ "Face used in tree mode for non-matching dates."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-subject-face
+ '((t (:foreground "gray")))
+ "Face used in tree mode for non-matching subjects."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-tree-face
+ '((t (:foreground "gray")))
+ "Face used in tree mode for the thread tree block graphics in messages matching the query."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-author-face
+ '((t (:foreground "gray")))
+ "Face used in tree mode for the date in messages matching the query."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-tag-face
+ '((t (:foreground "gray")))
+ "Face used in tree mode face for non-matching tags."
+ :group 'notmuch-tree
+ :group 'notmuch-faces)
+
+(defvar notmuch-tree-previous-subject
+ "The subject of the most recent result shown during the async display")
+(make-variable-buffer-local 'notmuch-tree-previous-subject)
+
+(defvar notmuch-tree-basic-query nil
+ "A buffer local copy of argument query to the function notmuch-tree")
+(make-variable-buffer-local 'notmuch-tree-basic-query)
+
+(defvar notmuch-tree-query-context nil
+ "A buffer local copy of argument query-context to the function notmuch-tree")
+(make-variable-buffer-local 'notmuch-tree-query-context)
+
+(defvar notmuch-tree-target-msg nil
+ "A buffer local copy of argument target to the function notmuch-tree")
+(make-variable-buffer-local 'notmuch-tree-target-msg)
+
+(defvar notmuch-tree-open-target nil
+ "A buffer local copy of argument open-target to the function notmuch-tree")
+(make-variable-buffer-local 'notmuch-tree-open-target)
+
+(defvar notmuch-tree-message-window nil
+ "The window of the message pane.
+
+It is set in both the tree buffer and the child show buffer. It
+is used to try and close the message pane when quitting tree view
+or the child show buffer.")
+(make-variable-buffer-local 'notmuch-tree-message-window)
+(put 'notmuch-tree-message-window 'permanent-local t)
+
+(defvar notmuch-tree-message-buffer nil
+ "The buffer name of the show buffer in the message pane.
+
+This is used to try and make sure we don't close the message pane
+if the user has loaded a different buffer in that window.")
+(make-variable-buffer-local 'notmuch-tree-message-buffer)
+(put 'notmuch-tree-message-buffer 'permanent-local t)
+
+(defun notmuch-tree-to-message-pane (func)
+ "Execute FUNC in message pane.
+
+This function returns a function (so can be used as a keybinding)
+which executes function FUNC in the message pane if it is
+open (if the message pane is closed it does nothing)."
+ `(lambda ()
+ ,(concat "(In message pane) " (documentation func t))
+ (interactive)
+ (when (window-live-p notmuch-tree-message-window)
+ (with-selected-window notmuch-tree-message-window
+ (call-interactively #',func)))))
+
+(defun notmuch-tree-button-activate (&optional button)
+ "Activate BUTTON or button at point
+
+This function does not give an error if there is no button."
+ (interactive)
+ (let ((button (or button (button-at (point)))))
+ (when button (button-activate button))))
+
+(defun notmuch-tree-close-message-pane-and (func)
+ "Close message pane and execute FUNC.
+
+This function returns a function (so can be used as a keybinding)
+which closes the message pane if open and then executes function
+FUNC."
+ `(lambda ()
+ ,(concat "(Close message pane and) " (documentation func t))
+ (interactive)
+ (notmuch-tree-close-message-window)
+ (call-interactively #',func)))
+
+(defvar notmuch-tree-mode-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map notmuch-common-keymap)
+ ;; The following override the global keymap.
+ ;; Override because we want to close message pane first.
+ (define-key map [remap notmuch-help] (notmuch-tree-close-message-pane-and #'notmuch-help))
+ ;; Override because we first close message pane and then close tree buffer.
+ (define-key map [remap notmuch-kill-this-buffer] 'notmuch-tree-quit)
+ ;; Override because we close message pane after the search query is entered.
+ (define-key map [remap notmuch-search] 'notmuch-tree-to-search)
+ ;; Override because we want to close message pane first.
+ (define-key map [remap notmuch-mua-new-mail] (notmuch-tree-close-message-pane-and #'notmuch-mua-new-mail))
+
+ ;; these use notmuch-show functions directly
+ (define-key map "|" 'notmuch-show-pipe-message)
+ (define-key map "w" 'notmuch-show-save-attachments)
+ (define-key map "v" 'notmuch-show-view-all-mime-parts)
+ (define-key map "c" 'notmuch-show-stash-map)
+
+ ;; these apply to the message pane
+ (define-key map (kbd "M-TAB") (notmuch-tree-to-message-pane #'notmuch-show-previous-button))
+ (define-key map (kbd "<backtab>") (notmuch-tree-to-message-pane #'notmuch-show-previous-button))
+ (define-key map (kbd "TAB") (notmuch-tree-to-message-pane #'notmuch-show-next-button))
+ (define-key map "e" (notmuch-tree-to-message-pane #'notmuch-tree-button-activate))
+
+ ;; bindings from show (or elsewhere) but we close the message pane first.
+ (define-key map "f" (notmuch-tree-close-message-pane-and #'notmuch-show-forward-message))
+ (define-key map "r" (notmuch-tree-close-message-pane-and #'notmuch-show-reply-sender))
+ (define-key map "R" (notmuch-tree-close-message-pane-and #'notmuch-show-reply))
+ (define-key map "V" (notmuch-tree-close-message-pane-and #'notmuch-show-view-raw-message))
+
+ ;; The main tree view bindings
+ (define-key map (kbd "RET") 'notmuch-tree-show-message)
+ (define-key map [mouse-1] 'notmuch-tree-show-message)
+ (define-key map "x" 'notmuch-tree-quit)
+ (define-key map "A" 'notmuch-tree-archive-thread)
+ (define-key map "a" 'notmuch-tree-archive-message-then-next)
+ (define-key map "=" 'notmuch-tree-refresh-view)
+ (define-key map "z" 'notmuch-tree-to-tree)
+ (define-key map "n" 'notmuch-tree-next-matching-message)
+ (define-key map "p" 'notmuch-tree-prev-matching-message)
+ (define-key map "N" 'notmuch-tree-next-message)
+ (define-key map "P" 'notmuch-tree-prev-message)
+ (define-key map (kbd "M-p") 'notmuch-tree-prev-thread)
+ (define-key map (kbd "M-n") 'notmuch-tree-next-thread)
+ (define-key map "-" 'notmuch-tree-remove-tag)
+ (define-key map "+" 'notmuch-tree-add-tag)
+ (define-key map "*" 'notmuch-tree-tag-thread)
+ (define-key map " " 'notmuch-tree-scroll-or-next)
+ (define-key map "b" 'notmuch-tree-scroll-message-window-back)
+ map))
+(fset 'notmuch-tree-mode-map notmuch-tree-mode-map)
+
+(defun notmuch-tree-get-message-properties ()
+ "Return the properties of the current message as a plist.
+
+Some useful entries are:
+:headers - Property list containing the headers :Date, :Subject, :From, etc.
+:tags - Tags for this message"
+ (save-excursion
+ (beginning-of-line)
+ (get-text-property (point) :notmuch-message-properties)))
+
+;; XXX This should really be a lib function but we are trying to
+;; reduce impact on the code base.
+(defun notmuch-show-get-prop (prop &optional props)
+ "This is a tree view overridden version of notmuch-show-get-prop
+
+It gets property PROP from PROPS or, if PROPS is nil, the current
+message in either tree or show. This means that several functions
+in notmuch-show now work unchanged in tree as they just need the
+correct message properties."
+ (let ((props (or props
+ (cond ((eq major-mode 'notmuch-show-mode)
+ (notmuch-show-get-message-properties))
+ ((eq major-mode 'notmuch-tree-mode)
+ (notmuch-tree-get-message-properties))))))
+ (plist-get props prop)))
+
+(defun notmuch-tree-set-message-properties (props)
+ (save-excursion
+ (beginning-of-line)
+ (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
+
+(defun notmuch-tree-set-prop (prop val &optional props)
+ (let ((inhibit-read-only t)
+ (props (or props
+ (notmuch-tree-get-message-properties))))
+ (plist-put props prop val)
+ (notmuch-tree-set-message-properties props)))
+
+(defun notmuch-tree-get-prop (prop &optional props)
+ (let ((props (or props
+ (notmuch-tree-get-message-properties))))
+ (plist-get props prop)))
+
+(defun notmuch-tree-set-tags (tags)
+ "Set the tags of the current message."
+ (notmuch-tree-set-prop :tags tags))
+
+(defun notmuch-tree-get-tags ()
+ "Return the tags of the current message."
+ (notmuch-tree-get-prop :tags))
+
+(defun notmuch-tree-get-message-id ()
+ "Return the message id of the current message."
+ (let ((id (notmuch-tree-get-prop :id)))
+ (if id
+ (notmuch-id-to-query id)
+ nil)))
+
+(defun notmuch-tree-get-match ()
+ "Return whether the current message is a match."
+ (interactive)
+ (notmuch-tree-get-prop :match))
+
+(defun notmuch-tree-refresh-result ()
+ "Redisplay the current message line.
+
+This redisplays the current line based on the messages
+properties (as they are now). This is used when tags are
+updated."
+ (let ((init-point (point))
+ (end (line-end-position))
+ (msg (notmuch-tree-get-message-properties))
+ (inhibit-read-only t))
+ (beginning-of-line)
+ ;; This is a little tricky: we override
+ ;; notmuch-tree-previous-subject to get the decision between
+ ;; ... and a subject right and it stops notmuch-tree-insert-msg
+ ;; from overwriting the buffer local copy of
+ ;; notmuch-tree-previous-subject if this is called while the
+ ;; buffer is displaying.
+ (let ((notmuch-tree-previous-subject (notmuch-tree-get-prop :previous-subject)))
+ (delete-region (point) (1+ (line-end-position)))
+ (notmuch-tree-insert-msg msg))
+ (let ((new-end (line-end-position)))
+ (goto-char (if (= init-point end)
+ new-end
+ (min init-point (- new-end 1)))))))
+
+(defun notmuch-tree-tag-update-display (&optional tag-changes)
+ "Update display for TAG-CHANGES to current message.
+
+Does NOT change the database."
+ (let* ((current-tags (notmuch-tree-get-tags))
+ (new-tags (notmuch-update-tags current-tags tag-changes)))
+ (unless (equal current-tags new-tags)
+ (notmuch-tree-set-tags new-tags)
+ (notmuch-tree-refresh-result))))
+
+(defun notmuch-tree-tag (tag-changes)
+ "Change tags for the current message"
+ (interactive
+ (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message")))
+ (notmuch-tag (notmuch-tree-get-message-id) tag-changes)
+ (notmuch-tree-tag-update-display tag-changes))
+
+(defun notmuch-tree-add-tag (tag-changes)
+ "Same as `notmuch-tree-tag' but sets initial input to '+'."
+ (interactive
+ (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message" "+")))
+ (notmuch-tree-tag tag-changes))
+
+(defun notmuch-tree-remove-tag (tag-changes)
+ "Same as `notmuch-tree-tag' but sets initial input to '-'."
+ (interactive
+ (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message" "-")))
+ (notmuch-tree-tag tag-changes))
+
+;; The next two functions close the message window before calling
+;; notmuch-search or notmuch-tree but they do so after the user has
+;; entered the query (in case the user was basing the query on
+;; something in the message window).
+
+(defun notmuch-tree-to-search ()
+ "Run \"notmuch search\" with the given `query' and display results."
+ (interactive)
+ (let ((query (notmuch-read-query "Notmuch search: ")))
+ (notmuch-tree-close-message-window)
+ (notmuch-search query)))
+
+(defun notmuch-tree-to-tree ()
+ "Run a query and display results in Tree view"
+ (interactive)
+ (let ((query (notmuch-read-query "Notmuch tree view search: ")))
+ (notmuch-tree-close-message-window)
+ (notmuch-tree query)))
+
+(defun notmuch-tree-message-window-kill-hook ()
+ "Close the message pane when exiting the show buffer."
+ (let ((buffer (current-buffer)))
+ (when (and (window-live-p notmuch-tree-message-window)
+ (eq (window-buffer notmuch-tree-message-window) buffer))
+ ;; We do not want an error if this is the sole window in the
+ ;; frame and I do not know how to test for that in emacs pre
+ ;; 24. Hence we just ignore-errors.
+ (ignore-errors
+ (delete-window notmuch-tree-message-window)))))
+
+(defun notmuch-tree-show-message-in ()
+ "Show the current message (in split-pane)."
+ (interactive)
+ (let ((id (notmuch-tree-get-message-id))
+ (inhibit-read-only t)
+ buffer)
+ (when id
+ ;; We close and reopen the window to kill off un-needed buffers
+ ;; this might cause flickering but seems ok.
+ (notmuch-tree-close-message-window)
+ (setq notmuch-tree-message-window
+ (split-window-vertically (/ (window-height) 4)))
+ (with-selected-window notmuch-tree-message-window
+ ;; Since we are only displaying one message do not indent.
+ (let ((notmuch-show-indent-messages-width 0)
+ (notmuch-show-only-matching-messages t))
+ (setq buffer (notmuch-show id))))
+ ;; We need the `let' as notmuch-tree-message-window is buffer local.
+ (let ((window notmuch-tree-message-window))
+ (with-current-buffer buffer
+ (setq notmuch-tree-message-window window)
+ (add-hook 'kill-buffer-hook 'notmuch-tree-message-window-kill-hook)))
+ (when notmuch-show-mark-read-tags
+ (notmuch-tree-tag-update-display notmuch-show-mark-read-tags))
+ (setq notmuch-tree-message-buffer buffer))))
+
+(defun notmuch-tree-show-message-out ()
+ "Show the current message (in whole window)."
+ (interactive)
+ (let ((id (notmuch-tree-get-message-id))
+ (inhibit-read-only t)
+ buffer)
+ (when id
+ ;; We close the window to kill off un-needed buffers.
+ (notmuch-tree-close-message-window)
+ (notmuch-show id))))
+
+(defun notmuch-tree-show-message (arg)
+ "Show the current message.
+
+Shows in split pane or whole window according to value of
+`notmuch-tree-show-out'. A prefix argument reverses the choice."
+ (interactive "P")
+ (if (or (and notmuch-tree-show-out (not arg))
+ (and (not notmuch-tree-show-out) arg))
+ (notmuch-tree-show-message-out)
+ (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-scroll-message-window ()
+ "Scroll the message window (if it exists)"
+ (interactive)
+ (when (window-live-p notmuch-tree-message-window)
+ (with-selected-window notmuch-tree-message-window
+ (if (pos-visible-in-window-p (point-max))
+ t
+ (scroll-up)))))
+
+(defun notmuch-tree-scroll-message-window-back ()
+ "Scroll the message window back(if it exists)"
+ (interactive)
+ (when (window-live-p notmuch-tree-message-window)
+ (with-selected-window notmuch-tree-message-window
+ (if (pos-visible-in-window-p (point-min))
+ t
+ (scroll-down)))))
+
+(defun notmuch-tree-scroll-or-next ()
+ "Scroll the message window. If it at end go to next message."
+ (interactive)
+ (when (notmuch-tree-scroll-message-window)
+ (notmuch-tree-next-matching-message)))
+
+(defun notmuch-tree-quit ()
+ "Close the split view or exit tree."
+ (interactive)
+ (unless (notmuch-tree-close-message-window)
+ (kill-buffer (current-buffer))))
+
+(defun notmuch-tree-close-message-window ()
+ "Close the message-window. Return t if close succeeds."
+ (interactive)
+ (when (and (window-live-p notmuch-tree-message-window)
+ (eq (window-buffer notmuch-tree-message-window) notmuch-tree-message-buffer))
+ (delete-window notmuch-tree-message-window)
+ (unless (get-buffer-window-list notmuch-tree-message-buffer)
+ (kill-buffer notmuch-tree-message-buffer))
+ t))
+
+(defun notmuch-tree-archive-message (&optional unarchive)
+ "Archive the current message.
+
+Archive the current message by applying the tag changes in
+`notmuch-archive-tags' to it. If a prefix argument is given, the
+message will be \"unarchived\", i.e. the tag changes in
+`notmuch-archive-tags' will be reversed."
+ (interactive "P")
+ (when notmuch-archive-tags
+ (notmuch-tree-tag (notmuch-tag-change-list notmuch-archive-tags unarchive))))
+
+(defun notmuch-tree-archive-message-then-next (&optional unarchive)
+ "Archive the current message and move to next matching message."
+ (interactive "P")
+ (notmuch-tree-archive-message unarchive)
+ (notmuch-tree-next-matching-message))
+
+(defun notmuch-tree-next-message ()
+ "Move to next message."
+ (interactive)
+ (forward-line)
+ (when (window-live-p notmuch-tree-message-window)
+ (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-prev-message ()
+ "Move to previous message."
+ (interactive)
+ (forward-line -1)
+ (when (window-live-p notmuch-tree-message-window)
+ (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-prev-matching-message ()
+ "Move to previous matching message."
+ (interactive)
+ (forward-line -1)
+ (while (and (not (bobp)) (not (notmuch-tree-get-match)))
+ (forward-line -1))
+ (when (window-live-p notmuch-tree-message-window)
+ (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-next-matching-message ()
+ "Move to next matching message."
+ (interactive)
+ (forward-line)
+ (while (and (not (eobp)) (not (notmuch-tree-get-match)))
+ (forward-line))
+ (when (window-live-p notmuch-tree-message-window)
+ (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-refresh-view ()
+ "Refresh view."
+ (interactive)
+ (let ((inhibit-read-only t)
+ (basic-query notmuch-tree-basic-query)
+ (query-context notmuch-tree-query-context)
+ (target (notmuch-tree-get-message-id)))
+ (erase-buffer)
+ (notmuch-tree-worker basic-query
+ query-context
+ target)))
+
+(defun notmuch-tree-thread-top ()
+ (when (notmuch-tree-get-message-properties)
+ (while (not (or (notmuch-tree-get-prop :first) (eobp)))
+ (forward-line -1))))
+
+(defun notmuch-tree-prev-thread ()
+ (interactive)
+ (forward-line -1)
+ (notmuch-tree-thread-top))
+
+(defun notmuch-tree-next-thread ()
+ (interactive)
+ (forward-line 1)
+ (while (not (or (notmuch-tree-get-prop :first) (eobp)))
+ (forward-line 1)))
+
+(defun notmuch-tree-thread-mapcar (function)
+ "Iterate through all messages in the current thread
+ and call FUNCTION for side effects."
+ (save-excursion
+ (notmuch-tree-thread-top)
+ (loop collect (funcall function)
+ do (forward-line)
+ while (and (notmuch-tree-get-message-properties)
+ (not (notmuch-tree-get-prop :first))))))
+
+(defun notmuch-tree-get-messages-ids-thread-search ()
+ "Return a search string for all message ids of messages in the current thread."
+ (mapconcat 'identity
+ (notmuch-tree-thread-mapcar 'notmuch-tree-get-message-id)
+ " or "))
+
+(defun notmuch-tree-tag-thread (tag-changes)
+ "Tag all messages in the current thread"
+ (interactive
+ (let ((tags (apply #'append (notmuch-tree-thread-mapcar
+ (lambda () (notmuch-tree-get-tags))))))
+ (list (notmuch-read-tag-changes tags "Tag thread"))))
+ (when (notmuch-tree-get-message-properties)
+ (notmuch-tag (notmuch-tree-get-messages-ids-thread-search) tag-changes)
+ (notmuch-tree-thread-mapcar
+ (lambda () (notmuch-tree-tag-update-display tag-changes)))))
+
+(defun notmuch-tree-archive-thread (&optional unarchive)
+ "Archive each message in thread.
+
+Archive each message currently shown by applying the tag changes
+in `notmuch-archive-tags' to each. If a prefix argument is given,
+the messages will be \"unarchived\", i.e. the tag changes in
+`notmuch-archive-tags' will be reversed.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+ (interactive "P")
+ (when notmuch-archive-tags
+ (notmuch-tree-tag-thread
+ (notmuch-tag-change-list notmuch-archive-tags unarchive))))
+
+;; Functions below here display the tree buffer itself.
+
+(defun notmuch-tree-clean-address (address)
+ "Try to clean a single email ADDRESS for display. Return
+AUTHOR_NAME if present, otherwise return AUTHOR_EMAIL. Return
+unchanged ADDRESS if parsing fails."
+ (let* ((clean-address (notmuch-clean-address address))
+ (p-address (car clean-address))
+ (p-name (cdr clean-address)))
+
+ ;; If we have a name return that otherwise return the address.
+ (or p-name p-address)))
+
+(defun notmuch-tree-format-field (field format-string msg)
+ "Format a FIELD of MSG according to FORMAT-STRING and return string"
+ (let* ((headers (plist-get msg :headers))
+ (match (plist-get msg :match)))
+ (cond
+ ((listp field)
+ (format format-string (notmuch-tree-format-field-list field msg)))
+
+ ((string-equal field "date")
+ (let ((face (if match
+ 'notmuch-tree-match-date-face
+ 'notmuch-tree-no-match-date-face)))
+ (propertize (format format-string (plist-get msg :date_relative)) 'face face)))
+
+ ((string-equal field "tree")
+ (let ((tree-status (plist-get msg :tree-status))
+ (face (if match
+ 'notmuch-tree-match-tree-face
+ 'notmuch-tree-no-match-tree-face)))
+
+ (propertize (format format-string
+ (mapconcat #'identity (reverse tree-status) ""))
+ 'face face)))
+
+ ((string-equal field "subject")
+ (let ((bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))
+ (previous-subject notmuch-tree-previous-subject)
+ (face (if match
+ 'notmuch-tree-match-subject-face
+ 'notmuch-tree-no-match-subject-face)))
+
+ (setq notmuch-tree-previous-subject bare-subject)
+ (propertize (format format-string
+ (if (string= previous-subject bare-subject)
+ " ..."
+ bare-subject))
+ 'face face)))
+
+ ((string-equal field "authors")
+ (let ((author (notmuch-tree-clean-address (plist-get headers :From)))
+ (len (length (format format-string "")))
+ (face (if match
+ 'notmuch-tree-match-author-face
+ 'notmuch-tree-no-match-author-face)))
+ (when (> (length author) len)
+ (setq author (substring author 0 len)))
+ (propertize (format format-string author) 'face face)))
+
+ ((string-equal field "tags")
+ (let ((tags (plist-get msg :tags))
+ (face (if match
+ 'notmuch-tree-match-tag-face
+ 'notmuch-tree-no-match-tag-face)))
+ (propertize (format format-string
+ (mapconcat #'identity tags ", "))
+ 'face face))))))
+
+
+(defun notmuch-tree-format-field-list (field-list msg)
+ "Format fields of MSG according to FIELD-LIST and return string"
+ (let (result-string)
+ (dolist (spec field-list result-string)
+ (let ((field-string (notmuch-tree-format-field (car spec) (cdr spec) msg)))
+ (setq result-string (concat result-string field-string))))))
+
+(defun notmuch-tree-insert-msg (msg)
+ "Insert the message MSG according to notmuch-tree-result-format"
+ ;; We need to save the previous subject as it will get overwritten
+ ;; by the insert-field calls.
+ (let ((previous-subject notmuch-tree-previous-subject))
+ (insert (notmuch-tree-format-field-list notmuch-tree-result-format msg))
+ (notmuch-tree-set-message-properties msg)
+ (notmuch-tree-set-prop :previous-subject previous-subject)
+ (insert "\n")))
+
+(defun notmuch-tree-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-tree-insert-msg msg))
+ (let ((msg-id (notmuch-id-to-query (plist-get msg :id)))
+ (target notmuch-tree-target-msg))
+ (when (or (and (not target) (plist-get msg :match))
+ (string= msg-id target))
+ (setq notmuch-tree-target-msg "found")
+ (goto-char (point-max))
+ (forward-line -1)
+ (when notmuch-tree-open-target
+ (notmuch-tree-show-message-in)))))
+
+(defun notmuch-tree-insert-tree (tree depth tree-status first last)
+ "Insert the message tree TREE at depth DEPTH in the current thread.
+
+A message tree is another name for a single sub-thread: i.e., a
+message together with all its descendents."
+ (let ((msg (car tree))
+ (replies (cadr tree)))
+
+ (cond
+ ((and (< 0 depth) (not last))
+ (push "├" tree-status))
+ ((and (< 0 depth) last)
+ (push "╰" tree-status))
+ ((and (eq 0 depth) first last)
+;; (push "─" tree-status)) choice between this and next line is matter of taste.
+ (push " " tree-status))
+ ((and (eq 0 depth) first (not last))
+ (push "┬" tree-status))
+ ((and (eq 0 depth) (not first) last)
+ (push "╰" tree-status))
+ ((and (eq 0 depth) (not first) (not last))
+ (push "├" tree-status)))
+
+ (push (concat (if replies "┬" "─") "►") tree-status)
+ (plist-put msg :first (and first (eq 0 depth)))
+ (notmuch-tree-goto-and-insert-msg (plist-put msg :tree-status tree-status))
+ (pop tree-status)
+ (pop tree-status)
+
+ (if last
+ (push " " tree-status)
+ (push "│" tree-status))
+
+ (notmuch-tree-insert-thread replies (1+ depth) tree-status)))
+
+(defun notmuch-tree-insert-thread (thread depth tree-status)
+ "Insert the collection of sibling sub-threads THREAD at depth DEPTH in the current forest."
+ (let ((n (length thread)))
+ (loop for tree in thread
+ for count from 1 to n
+
+ do (notmuch-tree-insert-tree tree depth tree-status (eq count 1) (eq count n)))))
+
+(defun notmuch-tree-insert-forest-thread (forest-thread)
+ "Insert a single complete thread."
+ (let (tree-status)
+ ;; Reset at the start of each main thread.
+ (setq notmuch-tree-previous-subject nil)
+ (notmuch-tree-insert-thread forest-thread 0 tree-status)))
+
+(defun notmuch-tree-insert-forest (forest)
+ "Insert a forest of threads.
+
+This function inserts a collection of several complete threads as
+passed to it by notmuch-tree-process-filter."
+ (mapc 'notmuch-tree-insert-forest-thread forest))
+
+(defun notmuch-tree-mode ()
+ "Major mode displaying messages (as opposed to threads) of of a notmuch search.
+
+This buffer contains the results of a \"notmuch tree\" of your
+email archives. Each line in the buffer represents a single
+message giving the relative date, the author, subject, and any
+tags.
+
+Pressing \\[notmuch-tree-show-message] on any line displays that message.
+
+Complete list of currently available key bindings:
+
+\\{notmuch-tree-mode-map}"
+
+ (interactive)
+ (kill-all-local-variables)
+ (setq notmuch-buffer-refresh-function #'notmuch-tree-refresh-view)
+ (use-local-map notmuch-tree-mode-map)
+ (setq major-mode 'notmuch-tree-mode
+ mode-name "notmuch-tree")
+ (hl-line-mode 1)
+ (setq buffer-read-only t
+ truncate-lines t))
+
+(defun notmuch-tree-process-sentinel (proc msg)
+ "Add a message to let user know when \"notmuch tree\" exits"
+ (let ((buffer (process-buffer proc))
+ (status (process-status proc))
+ (exit-status (process-exit-status proc))
+ (never-found-target-thread nil))
+ (when (memq status '(exit signal))
+ (kill-buffer (process-get proc 'parse-buf))
+ (if (buffer-live-p buffer)
+ (with-current-buffer buffer
+ (save-excursion
+ (let ((inhibit-read-only t)
+ (atbob (bobp)))
+ (goto-char (point-max))
+ (if (eq status 'signal)
+ (insert "Incomplete search results (tree view process was killed).\n"))
+ (when (eq status 'exit)
+ (insert "End of search results.")
+ (unless (= exit-status 0)
+ (insert (format " (process returned %d)" exit-status)))
+ (insert "\n")))))))))
+
+(defun notmuch-tree-process-filter (proc string)
+ "Process and filter the output of \"notmuch show\" for tree view"
+ (let ((results-buf (process-buffer proc))
+ (parse-buf (process-get proc 'parse-buf))
+ (inhibit-read-only t)
+ done)
+ (if (not (buffer-live-p results-buf))
+ (delete-process proc)
+ (with-current-buffer parse-buf
+ ;; Insert new data
+ (save-excursion
+ (goto-char (point-max))
+ (insert string))
+ (notmuch-sexp-parse-partial-list 'notmuch-tree-insert-forest-thread
+ results-buf)))))
+
+(defun notmuch-tree-worker (basic-query &optional query-context target open-target)
+ "Insert the tree view of the search in the current buffer.
+
+This is is a helper function for notmuch-tree. The arguments are
+the same as for the function notmuch-tree."
+ (interactive)
+ (notmuch-tree-mode)
+ (setq notmuch-tree-basic-query basic-query)
+ (setq notmuch-tree-query-context query-context)
+ (setq notmuch-tree-target-msg target)
+ (setq notmuch-tree-open-target open-target)
+
+ (erase-buffer)
+ (goto-char (point-min))
+ (let* ((search-args (concat basic-query
+ (if query-context (concat " and (" query-context ")"))
+ ))
+ (message-arg "--entire-thread"))
+ (if (equal (car (process-lines notmuch-command "count" search-args)) "0")
+ (setq search-args basic-query))
+ (let ((proc (notmuch-start-notmuch
+ "notmuch-tree" (current-buffer) #'notmuch-tree-process-sentinel
+ "show" "--body=false" "--format=sexp"
+ message-arg search-args))
+ ;; Use a scratch buffer to accumulate partial output.
+ ;; This buffer will be killed by the sentinel, which
+ ;; should be called no matter how the process dies.
+ (parse-buf (generate-new-buffer " *notmuch tree parse*")))
+ (process-put proc 'parse-buf parse-buf)
+ (set-process-filter proc 'notmuch-tree-process-filter)
+ (set-process-query-on-exit-flag proc nil))))
+
+(defun notmuch-tree (&optional query query-context target buffer-name open-target)
+ "Display threads matching QUERY in Tree View.
+
+The arguments are:
+ QUERY: the main query. This can be any query but in many cases will be
+ a single thread. If nil this is read interactively from the minibuffer.
+ QUERY-CONTEXT: is an additional term for the query. The query used
+ is QUERY and QUERY-CONTEXT unless that does not match any messages
+ in which case we fall back to just QUERY.
+ TARGET: A message ID (with the id: prefix) that will be made
+ current if it appears in the tree view results.
+ BUFFER-NAME: the name of the buffer to display the tree view. If
+ it is nil \"*notmuch-tree\" followed by QUERY is used.
+ OPEN-TARGET: If TRUE open the target message in the message pane."
+ (interactive)
+ (if (null query)
+ (setq query (notmuch-read-query "Notmuch tree view search: ")))
+ (let ((buffer (get-buffer-create (generate-new-buffer-name
+ (or buffer-name
+ (concat "*notmuch-tree-" query "*")))))
+ (inhibit-read-only t))
+
+ (switch-to-buffer buffer))
+ ;; Don't track undo information for this buffer
+ (set 'buffer-undo-list t)
+
+ (notmuch-tree-worker query query-context target open-target)
+
+ (setq truncate-lines t))
+
+
+;;
+
+(provide 'notmuch-tree)
(require 'notmuch-lib)
(require 'notmuch-tag)
(require 'notmuch-show)
+(require 'notmuch-tree)
(require 'notmuch-mua)
(require 'notmuch-hello)
(require 'notmuch-maildir-fcc)
(mm-save-part p))))
mm-handle))
-(defun notmuch-documentation-first-line (symbol)
- "Return the first line of the documentation string for SYMBOL."
- (let ((doc (documentation symbol)))
- (if doc
- (with-temp-buffer
- (insert (documentation symbol t))
- (goto-char (point-min))
- (let ((beg (point)))
- (end-of-line)
- (buffer-substring beg (point))))
- "")))
-
-(defun notmuch-prefix-key-description (key)
- "Given a prefix key code, return a human-readable string representation.
-
-This is basically just `format-kbd-macro' but we also convert ESC to M-."
- (let ((desc (format-kbd-macro (vector key))))
- (if (string= desc "ESC")
- "M-"
- (concat desc " "))))
-
-;; I would think that emacs would have code handy for walking a keymap
-;; and generating strings for each key, and I would prefer to just call
-;; that. But I couldn't find any (could be all implemented in C I
-;; suppose), so I wrote my own here.
-(defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
- "For a key binding, return a string showing a human-readable
-representation of the prefixed key as well as the first line of
-documentation from the bound function.
-
-For a mouse binding, return nil."
- (let ((key (car binding))
- (action (cdr binding)))
- (if (mouse-event-p key)
- nil
- (if (keymapp action)
- (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
- (as-list))
- (map-keymap (lambda (a b)
- (push (cons a b) as-list))
- action)
- (mapconcat substitute as-list "\n"))
- (concat prefix (format-kbd-macro (vector key))
- "\t"
- (notmuch-documentation-first-line action))))))
-
-(defun notmuch-substitute-command-keys-one (key)
- ;; A `keymap' key indicates inheritance from a parent keymap - the
- ;; inherited mappings follow, so there is nothing to print for
- ;; `keymap' itself.
- (when (not (eq key 'keymap))
- (notmuch-substitute-one-command-key-with-prefix nil key)))
-
-(defun notmuch-substitute-command-keys (doc)
- "Like `substitute-command-keys' but with documentation, not function names."
- (let ((beg 0))
- (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
- (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
- (keymap (symbol-value (intern keymap-name))))
- (setq doc (replace-match
- (mapconcat #'notmuch-substitute-command-keys-one
- (cdr keymap) "\n")
- 1 1 doc)))
- (setq beg (match-end 0)))
- doc))
-
-(defun notmuch-help ()
- "Display help for the current notmuch mode."
- (interactive)
- (let* ((mode major-mode)
- (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
- (with-current-buffer (generate-new-buffer "*notmuch-help*")
- (insert doc)
- (goto-char (point-min))
- (set-buffer-modified-p nil)
- (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
-
(require 'hl-line)
(defun notmuch-hl-line-mode ()
(defvar notmuch-search-mode-map
(let ((map (make-sparse-keymap)))
- (define-key map "?" 'notmuch-help)
- (define-key map "q" 'notmuch-search-quit)
- (define-key map "x" 'notmuch-search-quit)
+ (set-keymap-parent map notmuch-common-keymap)
+ (define-key map "x" 'notmuch-kill-this-buffer)
(define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
(define-key map "b" 'notmuch-search-scroll-down)
(define-key map " " 'notmuch-search-scroll-up)
(define-key map "n" 'notmuch-search-next-thread)
(define-key map "r" 'notmuch-search-reply-to-thread-sender)
(define-key map "R" 'notmuch-search-reply-to-thread)
- (define-key map "m" 'notmuch-mua-new-mail)
- (define-key map "s" 'notmuch-search)
(define-key map "o" 'notmuch-search-toggle-order)
(define-key map "c" 'notmuch-search-stash-map)
- (define-key map "=" 'notmuch-search-refresh-view)
- (define-key map "G" 'notmuch-search-poll-and-refresh-view)
(define-key map "t" 'notmuch-search-filter-by-tag)
(define-key map "f" 'notmuch-search-filter)
(define-key map [mouse-1] 'notmuch-search-show-thread)
(define-key map "-" 'notmuch-search-remove-tag)
(define-key map "+" 'notmuch-search-add-tag)
(define-key map (kbd "RET") 'notmuch-search-show-thread)
+ (define-key map "Z" 'notmuch-tree-from-search-current-query)
map)
"Keymap for \"notmuch search\" buffers.")
(fset 'notmuch-search-mode-map notmuch-search-mode-map)
(defvar notmuch-search-query-string)
(defvar notmuch-search-target-thread)
(defvar notmuch-search-target-line)
-(defvar notmuch-search-continuation)
(defvar notmuch-search-disjunctive-regexp "\\<[oO][rR]\\>")
-(defun notmuch-search-quit ()
- "Exit the search buffer, calling any defined continuation function."
- (interactive)
- (let ((continuation notmuch-search-continuation))
- (notmuch-kill-this-buffer)
- (when continuation
- (funcall continuation))))
-
(defun notmuch-search-scroll-up ()
"Move forward through search results by one window's worth."
(interactive)
(make-local-variable 'notmuch-search-oldest-first)
(make-local-variable 'notmuch-search-target-thread)
(make-local-variable 'notmuch-search-target-line)
- (set (make-local-variable 'notmuch-search-continuation) nil)
+ (setq notmuch-buffer-refresh-function #'notmuch-search-refresh-view)
(set (make-local-variable 'scroll-preserve-screen-position) t)
(add-to-invisibility-spec (cons 'ellipsis t))
(use-local-map notmuch-search-mode-map)
(let ((thread (plist-get (notmuch-search-get-result) :thread)))
(when thread (concat (unless bare "thread:") thread))))
-(defun notmuch-search-find-thread-id-region (beg end)
- "Return a list of threads for the current region"
- (mapcar (lambda (thread) (concat "thread:" thread))
- (notmuch-search-properties-in-region :thread beg end)))
+(defun notmuch-search-find-stable-query ()
+ "Return the stable queries for the current thread.
-(defun notmuch-search-find-thread-id-region-search (beg end)
- "Return a search string for threads for the current region"
- (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or "))
+This returns a list (MATCHED-QUERY UNMATCHED-QUERY) for the
+matched and unmatched messages in the current thread."
+ (plist-get (notmuch-search-get-result) :query))
+
+(defun notmuch-search-find-stable-query-region (beg end &optional only-matched)
+ "Return the stable query for the current region.
+
+If ONLY-MATCHED is non-nil, include only matched messages. If it
+is nil, include both matched and unmatched messages."
+ (let ((query-list nil) (all (not only-matched)))
+ (dolist (queries (notmuch-search-properties-in-region :query beg end))
+ (when (first queries)
+ (push (first queries) query-list))
+ (when (and all (second queries))
+ (push (second queries) query-list)))
+ (concat "(" (mapconcat 'identity query-list ") or (") ")")))
(defun notmuch-search-find-authors ()
"Return the authors for the current thread"
"Return a list of authors for the current region"
(notmuch-search-properties-in-region :subject beg end))
-(defun notmuch-search-show-thread ()
+(defun notmuch-search-show-thread (&optional elide-toggle)
"Display the currently selected thread."
- (interactive)
+ (interactive "P")
(let ((thread-id (notmuch-search-find-thread-id))
(subject (notmuch-search-find-subject)))
(if (> (length thread-id) 0)
(notmuch-show thread-id
+ elide-toggle
(current-buffer)
notmuch-search-query-string
;; Name the buffer based on the subject.
(concat "*" (truncate-string-to-width subject 30 nil nil t) "*"))
(message "End of search results."))))
+(defun notmuch-tree-from-search-current-query ()
+ "Call notmuch tree with the current query"
+ (interactive)
+ (notmuch-tree notmuch-search-query-string))
+
+(defun notmuch-tree-from-search-thread ()
+ "Show the selected thread with notmuch-tree"
+ (interactive)
+ (notmuch-tree (notmuch-search-find-thread-id)
+ notmuch-search-query-string
+ nil
+ (notmuch-prettify-subject (notmuch-search-find-subject))
+ t))
+
(defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
"Begin composing a reply-all to the entire current thread in a new buffer."
(interactive "P")
(let ((message-id (notmuch-search-find-thread-id)))
(notmuch-mua-new-reply message-id prompt-for-sender nil)))
-(defun notmuch-call-notmuch-process (&rest args)
- "Synchronously invoke \"notmuch\" with the given list of arguments.
-
-If notmuch exits with a non-zero status, output from the process
-will appear in a buffer named \"*Notmuch errors*\" and an error
-will be signaled."
- (with-temp-buffer
- (let ((status (apply #'call-process notmuch-command nil t nil args)))
- (notmuch-check-exit-status status (cons notmuch-command args)
- (buffer-string)))))
-
(defun notmuch-search-set-tags (tags &optional pos)
(let ((new-result (plist-put (notmuch-search-get-result pos) :tags tags)))
(notmuch-search-update-result new-result pos)))
(setq output (append output (notmuch-search-get-tags pos)))))
output))
-(defun notmuch-search-tag-region (beg end &optional tag-changes)
- "Change tags for threads in the given region."
- (let ((search-string (notmuch-search-find-thread-id-region-search beg end)))
- (setq tag-changes (notmuch-tag search-string tag-changes))
+(defun notmuch-search-interactive-region ()
+ "Return the bounds of the current interactive region.
+
+This returns (BEG END), where BEG and END are the bounds of the
+region if the region is active, or both `point' otherwise."
+ (if (region-active-p)
+ (list (region-beginning) (region-end))
+ (list (point) (point))))
+
+(defun notmuch-search-interactive-tag-changes (&optional initial-input)
+ "Prompt for tag changes for the current thread or region.
+
+Returns (TAG-CHANGES REGION-BEGIN REGION-END)."
+ (let* ((region (notmuch-search-interactive-region))
+ (beg (first region)) (end (second region))
+ (prompt (if (= beg end) "Tag thread" "Tag region")))
+ (cons (notmuch-read-tag-changes
+ (notmuch-search-get-tags-region beg end) prompt initial-input)
+ region)))
+
+(defun notmuch-search-tag (tag-changes &optional beg end only-matched)
+ "Change tags for the currently selected thread or region.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES.
+When called interactively, this uses the region if the region is
+active. When called directly, BEG and END provide the region.
+If these are nil or not provided, this applies to the thread at
+point.
+
+If ONLY-MATCHED is non-nil, only tag matched messages."
+ (interactive (notmuch-search-interactive-tag-changes))
+ (unless (and beg end) (setq beg (point) end (point)))
+ (let ((search-string (notmuch-search-find-stable-query-region
+ beg end only-matched)))
+ (notmuch-tag search-string tag-changes)
(notmuch-search-foreach-result beg end
(lambda (pos)
(notmuch-search-set-tags
(notmuch-update-tags (notmuch-search-get-tags pos) tag-changes)
pos)))))
-(defun notmuch-search-tag (&optional tag-changes)
- "Change tags for the currently selected thread or region.
+(defun notmuch-search-add-tag (tag-changes &optional beg end)
+ "Change tags for the current thread or region (defaulting to add).
-See `notmuch-tag' for information on the format of TAG-CHANGES."
- (interactive)
- (let* ((beg (if (region-active-p) (region-beginning) (point)))
- (end (if (region-active-p) (region-end) (point))))
- (notmuch-search-tag-region beg end tag-changes)))
+Same as `notmuch-search-tag' but sets initial input to '+'."
+ (interactive (notmuch-search-interactive-tag-changes "+"))
+ (notmuch-search-tag tag-changes beg end))
-(defun notmuch-search-add-tag ()
- "Same as `notmuch-search-tag' but sets initial input to '+'."
- (interactive)
- (notmuch-search-tag "+"))
+(defun notmuch-search-remove-tag (tag-changes &optional beg end)
+ "Change tags for the current thread or region (defaulting to remove).
-(defun notmuch-search-remove-tag ()
- "Same as `notmuch-search-tag' but sets initial input to '-'."
- (interactive)
- (notmuch-search-tag "-"))
+Same as `notmuch-search-tag' but sets initial input to '-'."
+ (interactive (notmuch-search-interactive-tag-changes "-"))
+ (notmuch-search-tag tag-changes beg end))
-(defun notmuch-search-archive-thread (&optional unarchive)
- "Archive the currently selected thread.
+(put 'notmuch-search-archive-thread 'notmuch-prefix-doc
+ "Un-archive the currently selected thread.")
+(defun notmuch-search-archive-thread (&optional unarchive beg end)
+ "Archive the currently selected thread or region.
Archive each message in the currently selected thread by applying
the tag changes in `notmuch-archive-tags' to each (remove the
`notmuch-archive-tags' will be reversed).
This function advances the next thread when finished."
- (interactive "P")
+ (interactive (cons current-prefix-arg (notmuch-search-interactive-region)))
(when notmuch-archive-tags
(notmuch-search-tag
- (notmuch-tag-change-list notmuch-archive-tags unarchive)))
+ (notmuch-tag-change-list notmuch-archive-tags unarchive) beg end))
(notmuch-search-next-thread))
(defun notmuch-search-update-result (result &optional pos)
(plist-get result :total)))
'face 'notmuch-search-count)))
((string-equal field "subject")
- (insert (propertize (format format-string (plist-get result :subject))
+ (insert (propertize (format format-string
+ (notmuch-sanitize (plist-get result :subject)))
'face 'notmuch-search-subject)))
((string-equal field "authors")
- (notmuch-search-insert-authors format-string (plist-get result :authors)))
+ (notmuch-search-insert-authors
+ format-string (notmuch-sanitize (plist-get result :authors))))
((string-equal field "tags")
(let ((tags (plist-get result :tags)))
(notmuch-sexp-parse-partial-list 'notmuch-search-show-result
results-buf)))))
-(defun notmuch-search-tag-all (&optional tag-changes)
+(defun notmuch-search-tag-all (tag-changes)
"Add/remove tags from all messages in current search buffer.
See `notmuch-tag' for information on the format of TAG-CHANGES."
- (interactive)
- (apply 'notmuch-tag notmuch-search-query-string tag-changes))
+ (interactive
+ (list (notmuch-read-tag-changes
+ (notmuch-search-get-tags-region (point-min) (point-max)) "Tag all")))
+ (notmuch-search-tag tag-changes (point-min) (point-max) t))
(defun notmuch-search-buffer-title (query)
"Returns the title for a buffer with notmuch search results."
'notmuch-search-history nil nil)))))
;;;###autoload
-(defun notmuch-search (&optional query oldest-first target-thread target-line continuation)
- "Run \"notmuch search\" with the given `query' and display results.
+(put 'notmuch-search 'notmuch-doc "Search for messages.")
+(defun notmuch-search (&optional query oldest-first target-thread target-line)
+ "Display threads matching QUERY in a notmuch-search buffer.
-If `query' is nil, it is read interactively from the minibuffer.
+If QUERY is nil, it is read interactively from the minibuffer.
Other optional parameters are used as follows:
- oldest-first: A Boolean controlling the sort order of returned threads
- target-thread: A thread ID (without the thread: prefix) that will be made
+ OLDEST-FIRST: A Boolean controlling the sort order of returned threads
+ TARGET-THREAD: A thread ID (without the thread: prefix) that will be made
current if it appears in the search results.
- target-line: The line number to move to if the target thread does not
- appear in the search results."
- (interactive)
+ TARGET-LINE: The line number to move to if the target thread does not
+ appear in the search results.
+
+When called interactively, this will prompt for a query and use
+the configured default sort order."
+ (interactive
+ (list
+ ;; Prompt for a query
+ nil
+ ;; Use the default search order (if we're doing a search from a
+ ;; search buffer, ignore any buffer-local overrides)
+ (default-value 'notmuch-search-oldest-first)))
+
(let* ((query (or query (notmuch-read-query "Notmuch search: ")))
(buffer (get-buffer-create (notmuch-search-buffer-title query))))
(switch-to-buffer buffer)
(set 'notmuch-search-oldest-first oldest-first)
(set 'notmuch-search-target-thread target-thread)
(set 'notmuch-search-target-line target-line)
- (set 'notmuch-search-continuation continuation)
(let ((proc (get-buffer-process (current-buffer)))
(inhibit-read-only t))
(if proc
(save-excursion
(let ((proc (notmuch-start-notmuch
"notmuch-search" buffer #'notmuch-search-process-sentinel
- "search" "--format=sexp" "--format-version=1"
+ "search" "--format=sexp" "--format-version=2"
(if oldest-first
"--sort=oldest-first"
"--sort=newest-first")
the new search results, then point will be placed on the same
thread. Otherwise, point will be moved to attempt to be in the
same relative position within the new buffer."
- (interactive)
(let ((target-line (line-number-at-pos))
(oldest-first notmuch-search-oldest-first)
(target-thread (notmuch-search-find-thread-id 'bare))
- (query notmuch-search-query-string)
- (continuation notmuch-search-continuation))
+ (query notmuch-search-query-string))
(notmuch-kill-this-buffer)
- (notmuch-search query oldest-first target-thread target-line continuation)
+ (notmuch-search query oldest-first target-thread target-line)
(goto-char (point-min))))
-(defcustom notmuch-poll-script nil
- "An external script to incorporate new mail into the notmuch database.
-
-This variable controls the action invoked by
-`notmuch-search-poll-and-refresh-view' and
-`notmuch-hello-poll-and-update' (each have a default keybinding
-of 'G') to incorporate new mail into the notmuch database.
-
-If set to nil (the default), new mail is processed by invoking
-\"notmuch new\". Otherwise, this should be set to a string that
-gives the name of an external script that processes new mail. If
-set to the empty string, no command will be run.
-
-The external script could do any of the following depending on
-the user's needs:
-
-1. Invoke a program to transfer mail to the local mail store
-2. Invoke \"notmuch new\" to incorporate the new mail
-3. Invoke one or more \"notmuch tag\" commands to classify the mail
-
-Note that the recommended way of achieving the same is using
-\"notmuch new\" hooks."
- :type '(choice (const :tag "notmuch new" nil)
- (const :tag "Disabled" "")
- (string :tag "Custom script"))
- :group 'notmuch-external)
-
-(defun notmuch-poll ()
- "Run \"notmuch new\" or an external script to import mail.
-
-Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
-depending on the value of `notmuch-poll-script'."
- (interactive)
- (if (stringp notmuch-poll-script)
- (unless (string= notmuch-poll-script "")
- (call-process notmuch-poll-script nil nil))
- (call-process notmuch-command nil nil nil "new")))
-
-(defun notmuch-search-poll-and-refresh-view ()
- "Invoke `notmuch-poll' to import mail, then refresh the current view."
- (interactive)
- (notmuch-poll)
- (notmuch-search-refresh-view))
-
(defun notmuch-search-toggle-order ()
"Toggle the current search order.
+++ /dev/null
-/*
- * Copyright © 2009 Keith Packard <keithp@keithp.com>
- * Copyright © 2010 Michal Sojka <sojkam1@fel.cvut.cz>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
- */
-
-#include "gmime-filter-headers.h"
-#include <string.h>
-#include <gmime/gmime-utils.h>
-#include <glib/gprintf.h>
-#include <stdlib.h>
-#include <xutil.h>
-
-/**
- * SECTION: gmime-filter-headers
- * @title: GMimeFilterHeaders
- * @short_description: Add/remove headers markers
- *
- * A #GMimeFilter for decoding rfc2047 encoded headers to UTF-8
- **/
-
-
-static void g_mime_filter_headers_class_init (GMimeFilterHeadersClass *klass);
-static void g_mime_filter_headers_init (GMimeFilterHeaders *filter, GMimeFilterHeadersClass *klass);
-static void g_mime_filter_headers_finalize (GObject *object);
-
-static GMimeFilter *filter_copy (GMimeFilter *filter);
-static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
- char **out, size_t *outlen, size_t *outprespace);
-static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
- char **out, size_t *outlen, size_t *outprespace);
-static void filter_reset (GMimeFilter *filter);
-
-
-static GMimeFilterClass *parent_class = NULL;
-
-GType
-g_mime_filter_headers_get_type (void)
-{
- static GType type = 0;
-
- if (!type) {
- static const GTypeInfo info = {
- sizeof (GMimeFilterHeadersClass),
- NULL, /* base_class_init */
- NULL, /* base_class_finalize */
- (GClassInitFunc) g_mime_filter_headers_class_init,
- NULL, /* class_finalize */
- NULL, /* class_data */
- sizeof (GMimeFilterHeaders),
- 0, /* n_preallocs */
- (GInstanceInitFunc) g_mime_filter_headers_init,
- NULL /* value_table */
- };
-
- type = g_type_register_static (GMIME_TYPE_FILTER, "GMimeFilterHeaders", &info, (GTypeFlags) 0);
- }
-
- return type;
-}
-
-
-static void
-g_mime_filter_headers_class_init (GMimeFilterHeadersClass *klass)
-{
- GObjectClass *object_class = G_OBJECT_CLASS (klass);
- GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
-
- parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
-
- object_class->finalize = g_mime_filter_headers_finalize;
-
- filter_class->copy = filter_copy;
- filter_class->filter = filter_filter;
- filter_class->complete = filter_complete;
- filter_class->reset = filter_reset;
-}
-
-static void
-g_mime_filter_headers_init (GMimeFilterHeaders *filter, GMimeFilterHeadersClass *klass)
-{
- (void) klass;
- filter->saw_nl = TRUE;
- filter->line = NULL;
- filter->line_size = 0;
- filter->lineptr = NULL;
-}
-
-static void
-g_mime_filter_headers_finalize (GObject *object)
-{
- free (GMIME_FILTER_HEADERS (object)->line);
- G_OBJECT_CLASS (parent_class)->finalize (object);
-}
-
-
-static GMimeFilter *
-filter_copy (GMimeFilter *filter)
-{
- (void) filter;
- return g_mime_filter_headers_new ();
-}
-
-static void
-output_decoded_header (GMimeFilterHeaders *headers, char **outptr)
-{
- char *colon, *name, *s, *decoded_value;
- size_t offset;
- gint ret;
-
- colon = strchr (headers->line, ':');
- if (colon == NULL)
- return;
-
- name = headers->line;
- *colon = '\0';
- s = colon + 1;
- while (*s == ' ' || *s == '\t')
- s++;
- decoded_value = g_mime_utils_header_decode_text(s);
- if (decoded_value == NULL)
- return;
- offset = *outptr - GMIME_FILTER (headers)->outbuf;
- g_mime_filter_set_size (GMIME_FILTER (headers), strlen(name) + 2 +
- strlen(decoded_value) + 2, TRUE);
- *outptr = GMIME_FILTER (headers)->outbuf + offset;
- ret = g_sprintf (*outptr, "%s: %s\n", name, decoded_value);
- if (ret > 0)
- *outptr += ret;
- free (decoded_value);
-}
-
-static void
-output_final_newline (GMimeFilterHeaders *headers, char **outptr)
-{
- size_t offset;
-
- offset = *outptr - GMIME_FILTER (headers)->outbuf;
- g_mime_filter_set_size (GMIME_FILTER (headers), 1, TRUE);
- *outptr = GMIME_FILTER (headers)->outbuf + offset;
- *(*outptr)++ = '\n';
-}
-
-static void
-filter_filter (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
- char **outbuf, size_t *outlen, size_t *outprespace)
-{
- GMimeFilterHeaders *headers = (GMimeFilterHeaders *) filter;
- register const char *inptr = inbuf;
- const char *inend = inbuf + inlen;
- char *lineptr, *lineend, *outptr;
-
- (void) prespace;
- if (headers->line == NULL) {
- headers->line_size = 200;
- headers->lineptr = headers->line = malloc (headers->line_size);
- }
- lineptr = headers->lineptr;
- lineend = headers->line + headers->line_size - 1;
- if (lineptr == NULL)
- return;
- outptr = filter->outbuf;
- while (inptr < inend) {
- if (*inptr == '\n') {
- if (headers->saw_nl)
- output_final_newline(headers, &outptr);
- headers->saw_nl = TRUE;
- inptr++;
- continue;
- }
-
- if (lineptr == lineend) {
- headers->line_size *= 2;
- headers->line = xrealloc (headers->line, headers->line_size);
- lineptr = headers->line + (headers->line_size / 2) - 1;
- lineend = headers->line + headers->line_size - 1;
- }
-
- if (headers->saw_nl && *inptr != ' ' && *inptr != '\t') {
- *lineptr = '\0';
- output_decoded_header (headers, &outptr);
- lineptr = headers->line;
- }
- if (headers->saw_nl && (*inptr == ' ' || *inptr == '\t')) {
- *lineptr = ' ';
- lineptr++;
- while (inptr < inend && (*inptr == ' ' || *inptr == '\t'))
- inptr++;
- headers->saw_nl = FALSE;
- continue;
- }
- headers->saw_nl = FALSE;
-
- if (*inptr != '\r')
- *lineptr++ = *inptr;
- inptr++;
- }
- if (headers->saw_nl) {
- *lineptr = '\0';
- output_decoded_header (headers, &outptr);
- lineptr = headers->line;
- }
- headers->lineptr = lineptr;
- *outlen = outptr - filter->outbuf;
- *outprespace = filter->outpre;
- *outbuf = filter->outbuf;
-}
-
-static void
-filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
- char **outbuf, size_t *outlen, size_t *outprespace)
-{
- if (inbuf && inlen)
- filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
-}
-
-static void
-filter_reset (GMimeFilter *filter)
-{
- GMimeFilterHeaders *headers = (GMimeFilterHeaders *) filter;
-
- headers->saw_nl = TRUE;
- free(headers->line);
- headers->line = NULL;
- headers->line_size = 0;
-}
-
-
-/**
- * g_mime_filter_headers_new:
- * @encode: %TRUE if the filter should encode or %FALSE otherwise
- * @dots: encode/decode dots (as for SMTP)
- *
- * Creates a new #GMimeFilterHeaders filter.
- *
- * If @encode is %TRUE, then all lines will be prefixed by "> ",
- * otherwise any lines starting with "> " will have that removed
- *
- * Returns: a new #GMimeFilterHeaders filter.
- **/
-GMimeFilter *
-g_mime_filter_headers_new (void)
-{
- GMimeFilterHeaders *new_headers;
-
- new_headers = (GMimeFilterHeaders *) g_object_newv (GMIME_TYPE_FILTER_HEADERS, 0, NULL);
-
- return (GMimeFilter *) new_headers;
-}
-
+++ /dev/null
-/*
- * Copyright © 2009 Keith Packard <keithp@keithp.com>
- * Copyright © 2010 Michal Sojka <sojkam1@fel.cvut.cz>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
- */
-
-#ifndef _GMIME_FILTER_HEADERS_H_
-#define _GMIME_FILTER_HEADERS_H_
-
-#include <gmime/gmime-filter.h>
-
-G_BEGIN_DECLS
-
-#define GMIME_TYPE_FILTER_HEADERS (g_mime_filter_headers_get_type ())
-#define GMIME_FILTER_HEADERS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GMIME_TYPE_FILTER_HEADERS, GMimeFilterHeaders))
-#define GMIME_FILTER_HEADERS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GMIME_TYPE_FILTER_HEADERS, GMimeFilterHeadersClass))
-#define GMIME_IS_FILTER_HEADERS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GMIME_TYPE_FILTER_HEADERS))
-#define GMIME_IS_FILTER_HEADERS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GMIME_TYPE_FILTER_HEADERS))
-#define GMIME_FILTER_HEADERS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GMIME_TYPE_FILTER_HEADERS, GMimeFilterHeadersClass))
-
-typedef struct _GMimeFilterHeaders GMimeFilterHeaders;
-typedef struct _GMimeFilterHeadersClass GMimeFilterHeadersClass;
-
-/**
- * GMimeFilterHeaders:
- * @parent_object: parent #GMimeFilter
- * @saw_nl: previous char was a \n
- * @line: temporary buffer for line unfolding
- * @line_size: size of currently allocated memory for @line
- * @lineptr: pointer to the first unused character in @line
- *
- * A filter to decode rfc2047 encoded headers
- **/
-struct _GMimeFilterHeaders {
- GMimeFilter parent_object;
-
- gboolean saw_nl;
- char *line;
- size_t line_size;
- char *lineptr;
-};
-
-struct _GMimeFilterHeadersClass {
- GMimeFilterClass parent_class;
-
-};
-
-
-GType g_mime_filter_headers_get_type (void);
-
-GMimeFilter *g_mime_filter_headers_new (void);
-
-G_END_DECLS
-
-
-#endif /* _GMIME_FILTER_HEADERS_H_ */
# the time of release for any additions to the library interface,
# (and when it is incremented, the release version of the library should
# be reset to 0).
-LIBNOTMUCH_VERSION_MINOR = 0
+LIBNOTMUCH_VERSION_MINOR = 1
# The release version the library interface. This should be incremented at
# the time of release if there have been no changes to the interface, (but
# simply compatible changes to the implementation).
LIBNOTMUCH_VERSION_RELEASE = 0
+# Note: Don't forget to change the VERSION macros in notmuch.h when
+# any of the above change.
+
ifeq ($(PLATFORM),MACOSX)
LIBRARY_SUFFIX = dylib
# On OS X, library version numbers go before suffix.
#include <iostream>
#include <sys/time.h>
+#include <sys/stat.h>
#include <signal.h>
+#include <ftw.h>
#include <glib.h> /* g_free, GPtrArray, GHashTable */
#include <glib-object.h> /* g_type_init */
return "Unbalanced number of calls to notmuch_message_freeze/thaw";
case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
return "Unbalanced number of calls to notmuch_database_begin_atomic/end_atomic";
+ case NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
+ return "Unsupported operation";
default:
case NOTMUCH_STATUS_LAST_STATUS:
return "Unknown error status value";
/* Initialize gmime */
if (! initialized) {
- g_mime_init (0);
+ g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
initialized = 1;
}
notmuch->date_range_processor = NULL;
}
+#if HAVE_XAPIAN_COMPACT
+static int
+unlink_cb (const char *path,
+ unused (const struct stat *sb),
+ unused (int type),
+ unused (struct FTW *ftw))
+{
+ return remove (path);
+}
+
+static int
+rmtree (const char *path)
+{
+ return nftw (path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
+}
+
+class NotmuchCompactor : public Xapian::Compactor
+{
+ notmuch_compact_status_cb_t status_cb;
+ void *status_closure;
+
+public:
+ NotmuchCompactor(notmuch_compact_status_cb_t cb, void *closure) :
+ status_cb (cb), status_closure (closure) { }
+
+ virtual void
+ set_status (const std::string &table, const std::string &status)
+ {
+ char *msg;
+
+ if (status_cb == NULL)
+ return;
+
+ if (status.length () == 0)
+ msg = talloc_asprintf (NULL, "compacting table %s", table.c_str());
+ else
+ msg = talloc_asprintf (NULL, " %s", status.c_str());
+
+ if (msg == NULL) {
+ return;
+ }
+
+ status_cb (msg, status_closure);
+ talloc_free (msg);
+ }
+};
+
+/* Compacts the given database, optionally saving the original database
+ * in backup_path. Additionally, a callback function can be provided to
+ * give the user feedback on the progress of the (likely long-lived)
+ * compaction process.
+ *
+ * The backup path must point to a directory on the same volume as the
+ * original database. Passing a NULL backup_path will result in the
+ * uncompacted database being deleted after compaction has finished.
+ * Note that the database write lock will be held during the
+ * compaction process to protect data integrity.
+ */
+notmuch_status_t
+notmuch_database_compact (const char *path,
+ const char *backup_path,
+ notmuch_compact_status_cb_t status_cb,
+ void *closure)
+{
+ void *local;
+ char *notmuch_path, *xapian_path, *compact_xapian_path;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ notmuch_database_t *notmuch = NULL;
+ struct stat statbuf;
+ notmuch_bool_t keep_backup;
+
+ local = talloc_new (NULL);
+ if (! local)
+ return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+ ret = notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much);
+ if (ret) {
+ goto DONE;
+ }
+
+ if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ if (! (compact_xapian_path = talloc_asprintf (local, "%s.compact", xapian_path))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ if (backup_path == NULL) {
+ if (! (backup_path = talloc_asprintf (local, "%s.old", xapian_path))) {
+ ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+ keep_backup = FALSE;
+ }
+ else {
+ keep_backup = TRUE;
+ }
+
+ if (stat (backup_path, &statbuf) != -1) {
+ fprintf (stderr, "Path already exists: %s\n", backup_path);
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+ if (errno != ENOENT) {
+ fprintf (stderr, "Unknown error while stat()ing path: %s\n",
+ strerror (errno));
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ /* Unconditionally attempt to remove old work-in-progress database (if
+ * any). This is "protected" by database lock. If this fails due to write
+ * errors (etc), the following code will fail and provide error message.
+ */
+ (void) rmtree (compact_xapian_path);
+
+ try {
+ NotmuchCompactor compactor (status_cb, closure);
+
+ compactor.set_renumber (false);
+ compactor.add_source (xapian_path);
+ compactor.set_destdir (compact_xapian_path);
+ compactor.compact ();
+ } catch (const Xapian::Error &error) {
+ fprintf (stderr, "Error while compacting: %s\n", error.get_msg().c_str());
+ ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+ goto DONE;
+ }
+
+ if (rename (xapian_path, backup_path)) {
+ fprintf (stderr, "Error moving %s to %s: %s\n",
+ xapian_path, backup_path, strerror (errno));
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ if (rename (compact_xapian_path, xapian_path)) {
+ fprintf (stderr, "Error moving %s to %s: %s\n",
+ compact_xapian_path, xapian_path, strerror (errno));
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ if (! keep_backup) {
+ if (rmtree (backup_path)) {
+ fprintf (stderr, "Error removing old database %s: %s\n",
+ backup_path, strerror (errno));
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+ }
+
+ DONE:
+ if (notmuch)
+ notmuch_database_destroy (notmuch);
+
+ talloc_free (local);
+
+ return ret;
+}
+#else
+notmuch_status_t
+notmuch_database_compact (unused (const char *path),
+ unused (const char *backup_path),
+ unused (notmuch_compact_status_cb_t status_cb),
+ unused (void *closure))
+{
+ fprintf (stderr, "notmuch was compiled against a xapian version lacking compaction support.\n");
+ return NOTMUCH_STATUS_UNSUPPORTED_OPERATION;
+}
+#endif
+
void
notmuch_database_destroy (notmuch_database_t *notmuch)
{
notmuch->last_doc_id++;
if (notmuch->last_doc_id == 0)
- INTERNAL_ERROR ("Xapian document IDs are exhausted.\n");
+ INTERNAL_ERROR ("Xapian document IDs are exhausted.\n");
return notmuch->last_doc_id;
}
static bool mbox_warning = false;
if (! initialized) {
- g_mime_init (0);
+ g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
initialized = 1;
}
*/
#include <string.h> /* for memcpy() etc. */
-
+#include "endian-util.h"
#include "libsha1.h"
#if defined(__cplusplus)
#define bswap_32(x) ((rotr32((x), 24) & 0x00ff00ff) | (rotr32((x), 8) & 0xff00ff00))
-#if (PLATFORM_BYTE_ORDER == IS_LITTLE_ENDIAN)
-#define bsw_32(p,n) \
- { int _i = (n); while(_i--) ((uint32_t*)p)[_i] = bswap_32(((uint32_t*)p)[_i]); }
+#if (UTIL_BYTE_ORDER == UTIL_ORDER_LITTLE_ENDIAN)
+# define bsw_32(p,n) \
+ { int _i = (n); while(_i--) ((uint32_t*)p)[_i] = bswap_32(((uint32_t*)p)[_i]); }
+#elif (UTIL_BYTE_ORDER == UTIL_ORDER_BIG_ENDIAN)
+# define bsw_32(p,n)
#else
-#define bsw_32(p,n)
+# error "Unsupported byte order"
#endif
#define SHA1_MASK (SHA1_BLOCK_SIZE - 1)
is_received = (strcmp(header_desired,"received") == 0);
if (! initialized) {
- g_mime_init (0);
+ g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
initialized = 1;
}
* compute the new maildir filename.
*
* If the existing filename is in the directory "new", the new
- * filename will be in the directory "cur".
+ * filename will be in the directory "cur", except for the case when
+ * no flags are changed and the existing filename does not contain
+ * maildir info (starting with ",2:").
*
* After a sequence of ":2," in the filename, any subsequent
* single-character flags will be added or removed according to the
char *filename_new, *dir;
char flag_map[128];
int flags_in_map = 0;
+ notmuch_bool_t flags_changed = FALSE;
unsigned int i;
char *s;
if (flag_map[flag] == 0) {
flag_map[flag] = 1;
flags_in_map++;
+ flags_changed = TRUE;
}
}
if (flag_map[flag]) {
flag_map[flag] = 0;
flags_in_map--;
+ flags_changed = TRUE;
}
}
+ /* Messages in new/ without maildir info can be kept in new/ if no
+ * flags have changed. */
+ dir = (char *) _filename_is_in_maildir (filename);
+ if (dir && STRNCMP_LITERAL (dir, "new/") == 0 && !*info && !flags_changed)
+ return talloc_strdup (ctx, filename);
+
filename_new = (char *) talloc_size (ctx,
info - filename +
strlen (":2,") + flags_in_map + 1);
typedef struct _notmuch_doc_id_set notmuch_doc_id_set_t;
-typedef struct _notmuch_string_list notmuch_string_list_t;
-
/* database.cc */
/* Lookup a prefix value by name.
unsigned int
_notmuch_directory_get_document_id (notmuch_directory_t *directory);
-/* thread.cc */
-
-notmuch_thread_t *
-_notmuch_thread_create (void *ctx,
- notmuch_database_t *notmuch,
- unsigned int seed_doc_id,
- notmuch_doc_id_set_t *match_set,
- notmuch_string_list_t *excluded_terms,
- notmuch_exclude_t omit_exclude,
- notmuch_sort_t sort);
-
/* message.cc */
notmuch_message_t *
notmuch_message_file_restrict_headersv (notmuch_message_file_t *message,
va_list va_headers);
-/* Get the value of the specified header from the message.
+/* Get the value of the specified header from the message as a UTF-8 string.
*
* The header name is case insensitive.
*
struct _notmuch_string_node *next;
} notmuch_string_node_t;
-struct visible _notmuch_string_list {
+typedef struct visible _notmuch_string_list {
int length;
notmuch_string_node_t *head;
notmuch_string_node_t **tail;
-};
+} notmuch_string_list_t;
notmuch_string_list_t *
_notmuch_string_list_create (const void *ctx);
_notmuch_filenames_create (const void *ctx,
notmuch_string_list_t *list);
+/* thread.cc */
+
+notmuch_thread_t *
+_notmuch_thread_create (void *ctx,
+ notmuch_database_t *notmuch,
+ unsigned int seed_doc_id,
+ notmuch_doc_id_set_t *match_set,
+ notmuch_string_list_t *excluded_terms,
+ notmuch_exclude_t omit_exclude,
+ notmuch_sort_t sort);
+
NOTMUCH_END_DECLS
#ifdef __cplusplus
#define TRUE 1
#endif
+/*
+ * The library version number. This must agree with the soname
+ * version in Makefile.local.
+ */
+#define LIBNOTMUCH_MAJOR_VERSION 3
+#define LIBNOTMUCH_MINOR_VERSION 1
+#define LIBNOTMUCH_MICRO_VERSION 0
+
+/*
+ * Check the version of the notmuch library being compiled against.
+ *
+ * Return true if the library being compiled against is of the
+ * specified version or above. For example:
+ *
+ * #if LIBNOTMUCH_CHECK_VERSION(3, 1, 0)
+ * (code requiring libnotmuch 3.1.0 or above)
+ * #endif
+ *
+ * LIBNOTMUCH_CHECK_VERSION has been defined since version 3.1.0; you
+ * can use #if !defined(NOTMUCH_CHECK_VERSION) to check for versions
+ * prior to that.
+ */
+#define LIBNOTMUCH_CHECK_VERSION (major, minor, micro) \
+ (LIBNOTMUCH_MAJOR_VERSION > (major) || \
+ (LIBNOTMUCH_MAJOR_VERSION == (major) && LIBNOTMUCH_MINOR_VERSION > (minor)) || \
+ (LIBNOTMUCH_MAJOR_VERSION == (major) && LIBNOTMUCH_MINOR_VERSION == (minor) && \
+ LIBNOTMUCH_MICRO_VERSION >= (micro)))
+
typedef int notmuch_bool_t;
/* Status codes used for the return values of most functions.
NOTMUCH_STATUS_TAG_TOO_LONG,
NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
NOTMUCH_STATUS_UNBALANCED_ATOMIC,
+ NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
NOTMUCH_STATUS_LAST_STATUS
} notmuch_status_t;
void
notmuch_database_close (notmuch_database_t *database);
+/* A callback invoked by notmuch_database_compact to notify the user
+ * of the progress of the compaction process.
+ */
+typedef void (*notmuch_compact_status_cb_t)(const char *message, void *closure);
+
+/* Compact a notmuch database, backing up the original database to the
+ * given path.
+ *
+ * The database will be opened with NOTMUCH_DATABASE_MODE_READ_WRITE
+ * during the compaction process to ensure no writes are made.
+ *
+ * If the optional callback function 'status_cb' is non-NULL, it will
+ * be called with diagnostic and informational messages. The argument
+ * 'closure' is passed verbatim to any callback invoked.
+ */
+notmuch_status_t
+notmuch_database_compact (const char* path,
+ const char* backup_path,
+ notmuch_compact_status_cb_t status_cb,
+ void *closure);
+
/* Destroy the notmuch database, closing it if necessary and freeing
-* all associated resources. */
+ * all associated resources.
+ */
void
notmuch_database_destroy (notmuch_database_t *database);
* provide progress indication to the user. If non-NULL it will be
* called periodically with 'progress' as a floating-point value in
* the range of [0.0 .. 1.0] indicating the progress made so far in
- * the upgrade process.
+ * the upgrade process. The argument 'closure' is passed verbatim to
+ * any callback invoked.
*/
notmuch_status_t
notmuch_database_upgrade (notmuch_database_t *database,
* This iterator will not necessarily iterate over all of the messages
* in the thread. It will only iterate over the messages in the thread
* which are not replies to other messages in the thread.
+ *
+ * The returned list will be destroyed when the thread is destroyed.
*/
notmuch_messages_t *
notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
/* Get a notmuch_thread_t iterator for all messages in 'thread' in
* oldest-first order.
+ *
+ * The returned list will be destroyed when the thread is destroyed.
*/
notmuch_messages_t *
notmuch_thread_get_messages (notmuch_thread_t *thread);
int
notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
-/* Get the authors of 'thread'
+/* Get the authors of 'thread' as a UTF-8 string.
*
* The returned string is a comma-separated list of the names of the
* authors of mail messages in the query results that belong to this
const char *
notmuch_thread_get_authors (notmuch_thread_t *thread);
-/* Get the subject of 'thread'
+/* Get the subject of 'thread' as a UTF-8 string.
*
* The subject is taken from the first message (according to the query
* order---see notmuch_query_set_sort) in the query results that
time_t
notmuch_message_get_date (notmuch_message_t *message);
-/* Get the value of the specified header from 'message'.
+/* Get the value of the specified header from 'message' as a UTF-8 string.
+ *
+ * Common headers are stored in the database when the message is
+ * indexed and will be returned from the database. Other headers will
+ * be read from the actual message file.
*
- * The value will be read from the actual message file, not from the
- * notmuch database. The header name is case insensitive.
+ * The header name is case insensitive.
*
* The returned string belongs to the message so should not be
* modified or freed by the caller (nor should it be referenced after
if (_debug_query ())
fprintf (stderr, "Query string is:\n%s\n", query_string);
- query = talloc (NULL, notmuch_query_t);
+ query = talloc (notmuch, notmuch_query_t);
if (unlikely (query == NULL))
return NULL;
MAN1 := \
$(MAIN_PAGE) \
+ $(dir)/man1/notmuch-compact.1 \
$(dir)/man1/notmuch-config.1 \
$(dir)/man1/notmuch-count.1 \
$(dir)/man1/notmuch-dump.1 \
--- /dev/null
+.TH NOTMUCH-COMPACT 1 2013-12-30 "Notmuch 0.17"
+.SH NAME
+notmuch-compact \- compact the notmuch database
+.SH SYNOPSIS
+
+.B notmuch compact
+.RI "[ --quiet ]"
+.RI "[ --backup=<" directory "> ]"
+
+.SH DESCRIPTION
+
+The
+.B compact
+command can be used to compact the notmuch database. This can both reduce
+the space required by the database and improve lookup performance.
+
+The compacted database is built in a temporary directory and is later
+moved into the place of the origin database. The original uncompacted
+database is discarded, unless the
+.BR "\-\-backup=" <directory>
+option is used.
+
+Note that the database write lock will be held during the compaction
+process (which may be quite long) to protect data integrity.
+
+Supported options for
+.B compact
+include
+
+.RS 4
+.TP 4
+.BR "\-\-backup=" <directory>
+
+Save the current database to the given directory before replacing it
+with the compacted database. The backup directory must not exist and
+it must reside on the same mounted filesystem as the current database.
+
+.RE
+
+.RS 4
+.TP 4
+.BR \-\-quiet
+
+Do not report database compaction progress to stdout.
+
+.RE
+
+.RE
+.SH ENVIRONMENT
+The following environment variables can be used to control the
+behavior of notmuch.
+.TP
+.B NOTMUCH_CONFIG
+Specifies the location of the notmuch configuration file. Notmuch will
+use ${HOME}/.notmuch\-config if this variable is not set.
+.SH SEE ALSO
+
+\fBnotmuch\fR(1), \fBnotmuch-count\fR(1), \fBnotmuch-dump\fR(1),
+\fBnotmuch-hooks\fR(5), \fBnotmuch-insert\fR(1), \fBnotmuch-new\fR(1),
+\fBnotmuch-reply\fR(1), \fBnotmuch-restore\fR(1), \fBnotmuch-search\fR(1),
+\fBnotmuch-search-terms\fR(7), \fBnotmuch-show\fR(1),
+\fBnotmuch-tag\fR(1)
-.TH NOTMUCH-CONFIG 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-CONFIG 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-config \- access notmuch configuration file
.SH SYNOPSIS
-.TH NOTMUCH-COUNT 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-COUNT 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-count \- count messages matching the given search terms
.SH SYNOPSIS
include
.RS 4
.TP 4
-.B \-\-output=(messages|threads)
+.B \-\-output=(messages|threads|files)
.RS 4
.TP 4
Output the number of matching threads.
.RE
+.RS 4
+.TP 4
+.B files
+
+Output the number of files associated with matching messages. This may
+be bigger than the number of matching messages due to duplicates
+(i.e. multiple files having the same message-id).
+.RE
.RE
.RS 4
-.TH NOTMUCH-DUMP 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-DUMP 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-dump \- creates a plain-text dump of the tags of each message
-.TH NOTMUCH-INSERT 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-INSERT 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-insert \- add a message to the maildir and notmuch database
.SH SYNOPSIS
-.TH NOTMUCH-NEW 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-NEW 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-new \- incorporate new mail into the notmuch database
.SH SYNOPSIS
-.TH NOTMUCH-REPLY 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-REPLY 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-reply \- constructs a reply template for a set of messages
.RS
.TP 4
.BR default
-Includes subject and quoted message body.
+Includes subject and quoted message body as an RFC 2822 message.
.TP
.BR json
Produces JSON output containing headers for a reply message and the
-.TH NOTMUCH-RESTORE 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-RESTORE 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-restore \- restores the tags from the given file (see notmuch dump)
-.TH NOTMUCH-SEARCH 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-SEARCH 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-search \- search for messages matching the given search terms
.SH SYNOPSIS
one per line (\-\-format=text), separated by null characters
(\-\-format=text0), as a JSON array (\-\-format=json), or as an
S-Expression list (\-\-format=sexp).
+
+Note that each message may have multiple filenames associated with it.
+All of them are included in the output, unless limited with the
+\-\-duplicate=N option.
.RE
.RS 4
.TP 4
thread, rather than the number of matching messages.
.RE
+.RS 4
+.TP 4
+.BR \-\-duplicate=N
+
+Effective with
+.BR --output=files ,
+output the Nth filename associated with each message matching the
+query (N is 1-based). If N is greater than the number of files
+associated with the message, don't print anything.
+
+Note that this option is orthogonal with the
+.BR folder:
+search prefix. The prefix matches messages based on filenames. This
+option filters filenames of the matching messages.
+.RE
+
.SH EXIT STATUS
This command supports the following special exit status codes
-.TH NOTMUCH-SHOW 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-SHOW 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-show \- show messages matching the given search terms
.SH SYNOPSIS
output is much faster and substantially smaller.
.RE
+.RS 4
+.TP 4
+.B \-\-include-html
+
+Include "text/html" parts as part of the output (currently only supported with
+--format=json and --format=sexp).
+By default, unless
+.B --part=N
+is used to select a specific part or
+.B --include-html
+is used to include all "text/html" parts, no part with content type "text/html"
+is included in the output.
+.RE
+
A common use of
.B notmuch show
is to display a single thread of email messages. For this, use a
-.TH NOTMUCH-TAG 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-TAG 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-tag \- add/remove tags for all messages matching the search terms
.\" along with this program. If not, see http://www.gnu.org/licenses/ .
.\"
.\" Author: Carl Worth <cworth@cworth.org>
-.TH NOTMUCH 1 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH 1 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch \- thread-based email index, search, and tagging
.SH SYNOPSIS
-.TH NOTMUCH-HOOKS 5 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-HOOKS 5 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-hooks \- hooks for notmuch
-.TH NOTMUCH-SEARCH-TERMS 7 2013-08-03 "Notmuch 0.16"
+.TH NOTMUCH-SEARCH-TERMS 7 2013-12-30 "Notmuch 0.17"
.SH NAME
notmuch-search-terms \- syntax for notmuch queries
The
.B folder:
prefix can be used to search for email message files that are
-contained within particular directories within the mail store. Only
-the directory components below the top-level mail database path are
-available to be searched.
+contained within particular directories within the mail store. If the
+same email message has multiple message files associated with it, it's
+sufficient for a match that at least one of the files is contained
+within a matching directory. Only the directory components below the
+top-level mail database path are available to be searched.
The
.B date:
notmuch_bool_t raw;
int part;
notmuch_crypto_t crypto;
+ notmuch_bool_t include_html;
} notmuch_show_params_t;
/* There's no point in continuing when we've detected that we've done
* this. New (required) map fields can be added without increasing
* this.
*/
-#define NOTMUCH_FORMAT_CUR 1
+#define NOTMUCH_FORMAT_CUR 2
/* The minimum supported structured output format version. Requests
* for format versions below this will return an error. */
#define NOTMUCH_FORMAT_MIN 1
+/* The minimum non-deprecated structured output format version.
+ * Requests for format versions below this will print a stern warning.
+ * Must be between NOTMUCH_FORMAT_MIN and NOTMUCH_FORMAT_CUR,
+ * inclusive.
+ */
+#define NOTMUCH_FORMAT_MIN_ACTIVE 1
/* The output format version requested by the caller on the command
* line. If no format version is requested, this will be set to
int
notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]);
+int
+notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[]);
+
const char *
notmuch_time_relative_date (const void *ctx, time_t then);
void
format_part_sprinter (const void *ctx, struct sprinter *sp, mime_node_t *node,
- notmuch_bool_t first, notmuch_bool_t output_body);
+ notmuch_bool_t first, notmuch_bool_t output_body,
+ notmuch_bool_t include_html);
void
format_headers_sprinter (struct sprinter *sp, GMimeMessage *message,
--- /dev/null
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2013 Ben Gamari
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Ben Gamari <bgamari.foss@gmail.com>
+ */
+
+#include "notmuch-client.h"
+
+static void
+status_update_cb (const char *msg, unused (void *closure))
+{
+ printf ("%s\n", msg);
+}
+
+int
+notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[])
+{
+ const char *path = notmuch_config_get_database_path (config);
+ const char *backup_path = NULL;
+ notmuch_status_t ret;
+ notmuch_bool_t quiet;
+ int opt_index;
+
+ notmuch_opt_desc_t options[] = {
+ { NOTMUCH_OPT_STRING, &backup_path, "backup", 0, 0 },
+ { NOTMUCH_OPT_BOOLEAN, &quiet, "quiet", 'q', 0 },
+ };
+
+ opt_index = parse_arguments (argc, argv, options, 1);
+ if (opt_index < 0)
+ return 1;
+
+ if (! quiet)
+ printf ("Compacting database...\n");
+ ret = notmuch_database_compact (path, backup_path,
+ quiet ? NULL : status_update_cb, NULL);
+ if (ret) {
+ fprintf (stderr, "Compaction failed: %s\n", notmuch_status_to_string (ret));
+ return 1;
+ }
+
+ if (! quiet) {
+ if (backup_path)
+ printf ("The old database has been moved to %s.\n", backup_path);
+
+ printf ("Done.\n");
+ }
+
+ return 0;
+}
*group = item;
- period = index (item, '.');
+ period = strchr (item, '.');
if (period == NULL || *(period+1) == '\0') {
fprintf (stderr,
"Invalid configuration name: %s\n"
enum {
OUTPUT_THREADS,
OUTPUT_MESSAGES,
+ OUTPUT_FILES,
};
/* The following is to allow future options to be added more easily */
EXCLUDE_FALSE,
};
+static unsigned int
+count_files (notmuch_query_t *query)
+{
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ notmuch_filenames_t *filenames;
+ unsigned int count = 0;
+
+ messages = notmuch_query_search_messages (query);
+ if (messages == NULL)
+ return 0;
+
+ for (;
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages)) {
+ message = notmuch_messages_get (messages);
+ filenames = notmuch_message_get_filenames (message);
+
+ for (;
+ notmuch_filenames_valid (filenames);
+ notmuch_filenames_move_to_next (filenames))
+ count++;
+
+ notmuch_filenames_destroy (filenames);
+ notmuch_message_destroy (message);
+ }
+
+ notmuch_messages_destroy (messages);
+
+ return count;
+}
+
static int
print_count (notmuch_database_t *notmuch, const char *query_str,
const char **exclude_tags, size_t exclude_tags_length, int output)
case OUTPUT_THREADS:
printf ("%u\n", notmuch_query_count_threads (query));
break;
+ case OUTPUT_FILES:
+ printf ("%u\n", count_files (query));
+ break;
}
notmuch_query_destroy (query);
{ NOTMUCH_OPT_KEYWORD, &output, "output", 'o',
(notmuch_keyword_t []){ { "threads", OUTPUT_THREADS },
{ "messages", OUTPUT_MESSAGES },
+ { "files", OUTPUT_FILES },
{ 0, 0 } } },
{ NOTMUCH_OPT_KEYWORD, &exclude, "exclude", 'x',
(notmuch_keyword_t []){ { "true", EXCLUDE_TRUE },
}
db_mtime = directory ? notmuch_directory_get_mtime (directory) : 0;
+ /* If the directory is unchanged from our last scan and has no
+ * sub-directories, then return without scanning it at all. In
+ * some situations, skipping the scan can substantially reduce the
+ * cost of notmuch new, especially since the huge numbers of files
+ * in Maildirs make scans expensive, but all files live in leaf
+ * directories.
+ *
+ * To check for sub-directories, we borrow a trick from find,
+ * kpathsea, and many other UNIX tools: since a directory's link
+ * count is the number of sub-directories (specifically, their
+ * '..' entries) plus 2 (the link from the parent and the link for
+ * '.'). This check is safe even on weird file systems, since
+ * file systems that can't compute this will return 0 or 1. This
+ * is safe even on *really* weird file systems like HFS+ that
+ * mistakenly return the total number of directory entries, since
+ * that only inflates the count beyond 2.
+ */
+ if (directory && fs_mtime == db_mtime && st.st_nlink == 2) {
+ /* There's one catch: pass 1 below considers symlinks to
+ * directories to be directories, but these don't increase the
+ * file system link count. So, only bail early if the
+ * database agrees that there are no sub-directories. */
+ db_subdirs = notmuch_directory_get_child_directories (directory);
+ if (!notmuch_filenames_valid (db_subdirs))
+ goto DONE;
+ notmuch_filenames_destroy (db_subdirs);
+ db_subdirs = NULL;
+ }
+
/* If the database knows about this directory, then we sort based
* on strcmp to match the database sorting. Otherwise, we can do
* inode-based sorting for faster filesystem operation. */
*/
#include "notmuch-client.h"
-#include "gmime-filter-headers.h"
#include "sprinter.h"
static void
show_reply_headers (GMimeMessage *message)
{
- GMimeStream *stream_stdout = NULL, *stream_filter = NULL;
+ GMimeStream *stream_stdout = NULL;
stream_stdout = g_mime_stream_file_new (stdout);
if (stream_stdout) {
g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
- stream_filter = g_mime_stream_filter_new(stream_stdout);
- if (stream_filter) {
- g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
- g_mime_filter_headers_new());
- g_mime_object_write_to_stream(GMIME_OBJECT(message), stream_filter);
- g_object_unref(stream_filter);
- }
+ /* Output RFC 2822 formatted (and RFC 2047 encoded) headers. */
+ g_mime_object_write_to_stream (GMIME_OBJECT(message), stream_stdout);
g_object_unref(stream_stdout);
}
}
"In-Reply-To", in_reply_to);
orig_references = notmuch_message_get_header (message, "references");
+ if (!orig_references)
+ /* Treat errors like missing References headers. */
+ orig_references = "";
references = talloc_asprintf (ctx, "%s%s%s",
- orig_references ? orig_references : "",
- orig_references ? " " : "",
+ *orig_references ? orig_references : "",
+ *orig_references ? " " : "",
in_reply_to);
g_mime_object_set_header (GMIME_OBJECT (reply),
"References", references);
/* Start the original */
sp->map_key (sp, "original");
- format_part_sprinter (ctx, sp, node, TRUE, TRUE);
+ format_part_sprinter (ctx, sp, node, TRUE, TRUE, FALSE);
/* End */
sp->end (sp);
#include "notmuch-client.h"
#include "sprinter.h"
+#include "string-util.h"
typedef enum {
OUTPUT_SUMMARY,
return out;
}
+/* Return two stable query strings that identify exactly the matched
+ * and unmatched messages currently in thread. If there are no
+ * matched or unmatched messages, the returned buffers will be
+ * NULL. */
+static int
+get_thread_query (notmuch_thread_t *thread,
+ char **matched_out, char **unmatched_out)
+{
+ notmuch_messages_t *messages;
+ char *escaped = NULL;
+ size_t escaped_len = 0;
+
+ *matched_out = *unmatched_out = NULL;
+
+ for (messages = notmuch_thread_get_messages (thread);
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ notmuch_message_t *message = notmuch_messages_get (messages);
+ const char *mid = notmuch_message_get_message_id (message);
+ /* Determine which query buffer to extend */
+ char **buf = notmuch_message_get_flag (
+ message, NOTMUCH_MESSAGE_FLAG_MATCH) ? matched_out : unmatched_out;
+ /* Add this message's id: query. Since "id" is an exclusive
+ * prefix, it is implicitly 'or'd together, so we only need to
+ * join queries with a space. */
+ if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
+ return -1;
+ if (*buf)
+ *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
+ else
+ *buf = talloc_strdup (thread, escaped);
+ if (!*buf)
+ return -1;
+ }
+ talloc_free (escaped);
+ return 0;
+}
+
static int
do_search_threads (sprinter_t *format,
notmuch_query_t *query,
format->string (format, authors);
format->map_key (format, "subject");
format->string (format, subject);
+ if (notmuch_format_version >= 2) {
+ char *matched_query, *unmatched_query;
+ if (get_thread_query (thread, &matched_query,
+ &unmatched_query) < 0) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+ format->map_key (format, "query");
+ format->begin_list (format);
+ if (matched_query)
+ format->string (format, matched_query);
+ else
+ format->null (format);
+ if (unmatched_query)
+ format->string (format, unmatched_query);
+ else
+ format->null (format);
+ format->end (format);
+ }
}
talloc_free (ctx_quote);
notmuch_query_t *query,
output_t output,
int offset,
- int limit)
+ int limit,
+ int dupe)
{
notmuch_message_t *message;
notmuch_messages_t *messages;
message = notmuch_messages_get (messages);
if (output == OUTPUT_FILES) {
+ int j;
filenames = notmuch_message_get_filenames (message);
- for (;
+ for (j = 1;
notmuch_filenames_valid (filenames);
- notmuch_filenames_move_to_next (filenames))
+ notmuch_filenames_move_to_next (filenames), j++)
{
- format->string (format, notmuch_filenames_get (filenames));
- format->separator (format);
+ if (dupe < 0 || dupe == j) {
+ format->string (format, notmuch_filenames_get (filenames));
+ format->separator (format);
+ }
}
notmuch_filenames_destroy( filenames );
int offset = 0;
int limit = -1; /* unlimited */
notmuch_exclude_t exclude = NOTMUCH_EXCLUDE_TRUE;
+ int dupe = -1;
unsigned int i;
enum {
{ 0, 0 } } },
{ NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
{ NOTMUCH_OPT_INT, &limit, "limit", 'L', 0 },
+ { NOTMUCH_OPT_INT, &dupe, "duplicate", 'D', 0 },
{ 0, 0, 0, 0, 0 }
};
break;
case OUTPUT_MESSAGES:
case OUTPUT_FILES:
- ret = do_search_messages (format, query, output, offset, limit);
+ ret = do_search_messages (format, query, output, offset, limit, dupe);
break;
case OUTPUT_TAGS:
ret = do_search_tags (notmuch, format, query);
void
format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
- notmuch_bool_t first, notmuch_bool_t output_body)
+ notmuch_bool_t first, notmuch_bool_t output_body,
+ notmuch_bool_t include_html)
{
/* Any changes to the JSON or S-Expression format should be
* reflected in the file devel/schemata. */
if (output_body) {
sp->map_key (sp, "body");
sp->begin_list (sp);
- format_part_sprinter (ctx, sp, mime_node_child (node, 0), first, TRUE);
+ format_part_sprinter (ctx, sp, mime_node_child (node, 0), first, TRUE, include_html);
sp->end (sp);
}
sp->end (sp);
/* For non-HTML text parts, we include the content in the
* JSON. Since JSON must be Unicode, we handle charset
* decoding here and do not report a charset to the caller.
- * For text/html parts, we do not include the content. If a
- * caller is interested in text/html parts, it should retrieve
- * them separately and they will not be decoded. Since this
- * makes charset decoding the responsibility on the caller, we
+ * For text/html parts, we do not include the content unless
+ * the --include-html option has been passed. If a html part
+ * is not included, it can be requested directly. This makes
+ * charset decoding the responsibility on the caller so we
* report the charset for text/html parts.
*/
if (g_mime_content_type_is_type (content_type, "text", "*") &&
- ! g_mime_content_type_is_type (content_type, "text", "html"))
+ (include_html ||
+ ! g_mime_content_type_is_type (content_type, "text", "html")))
{
GMimeStream *stream_memory = g_mime_stream_mem_new ();
GByteArray *part_content;
}
for (i = 0; i < node->nchildren; i++)
- format_part_sprinter (ctx, sp, mime_node_child (node, i), i == 0, TRUE);
+ format_part_sprinter (ctx, sp, mime_node_child (node, i), i == 0, TRUE, include_html);
/* Close content structures */
for (i = 0; i < nclose; i++)
mime_node_t *node, unused (int indent),
const notmuch_show_params_t *params)
{
- format_part_sprinter (ctx, sp, node, TRUE, params->output_body);
+ format_part_sprinter (ctx, sp, node, TRUE, params->output_body, params->include_html);
return NOTMUCH_STATUS_SUCCESS;
}
.crypto = {
.verify = FALSE,
.decrypt = FALSE
- }
+ },
+ .include_html = FALSE
};
int format_sel = NOTMUCH_FORMAT_NOT_SPECIFIED;
int exclude = EXCLUDE_TRUE;
{ NOTMUCH_OPT_BOOLEAN, ¶ms.crypto.decrypt, "decrypt", 'd', 0 },
{ NOTMUCH_OPT_BOOLEAN, ¶ms.crypto.verify, "verify", 'v', 0 },
{ NOTMUCH_OPT_BOOLEAN, ¶ms.output_body, "body", 'b', 0 },
+ { NOTMUCH_OPT_BOOLEAN, ¶ms.include_html, "include-html", 0, 0 },
{ 0, 0, 0, 0, 0 }
};
}
}
+ if (params.include_html &&
+ (format_sel != NOTMUCH_FORMAT_JSON && format_sel != NOTMUCH_FORMAT_SEXP)) {
+ fprintf (stderr, "Warning: --include-html only implemented for format=json and format=sexp\n");
+ }
+
if (entire_thread == ENTIRE_THREAD_TRUE)
params.entire_thread = TRUE;
else
"Create a plain-text dump of the tags for each message." },
{ "restore", notmuch_restore_command, FALSE,
"Restore the tags from the given dump file (see 'dump')." },
+ { "compact", notmuch_compact_command, FALSE,
+ "Compact the notmuch database." },
{ "config", notmuch_config_command, FALSE,
"Get or set settings in the notmuch configuration file." },
{ "help", notmuch_help_command, TRUE, /* create but don't save config */
upgrade your notmuch front-end.\n",
notmuch_format_version, NOTMUCH_FORMAT_MIN);
exit (NOTMUCH_EXIT_FORMAT_TOO_OLD);
- } else if (notmuch_format_version != NOTMUCH_FORMAT_CUR) {
+ } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN_ACTIVE) {
/* Warn about old version requests so compatibility issues are
* less likely when we drop support for a deprecated format
* versions. */
local = talloc_new (NULL);
- g_mime_init (0);
+ g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
#if !GLIB_CHECK_VERSION(2, 35, 1)
g_type_init ();
#endif
#include <sys/time.h>
#include <sys/types.h>
+#include "compat.h"
#include "parse-time-string.h"
/*
download-corpus:
wget -O ${TXZFILE} ${DEFAULT_URL}
-CLEAN := $(CLEAN) $(dir)/tmp.* $(dir)/log.* $(dir)/corpus $(dir)/notmuch.cache.*
+CLEAN := $(CLEAN) $(dir)/tmp.* $(dir)/log.*
+DISTCLEAN := $(dir)/corpus $(dir)/notmuch.cache.*
$(dir)/smtp-dummy: $(smtp_dummy_modules)
$(call quiet,CC) $^ -o $@
-$(dir)/symbol-test: $(dir)/symbol-test.o
+$(dir)/symbol-test: $(dir)/symbol-test.o lib/$(LINKER_NAME)
$(call quiet,CXX) $^ -o $@ -Llib -lnotmuch $(XAPIAN_LDFLAGS)
$(dir)/parse-time: $(dir)/parse-time.o parse-time-string/parse-time-string.o
#!/usr/bin/env bash
+set -eu
+
fixed=0
success=0
failed=0
tests=$(pluralize "test" $skipped)
echo "$skipped $tests skipped."
fi
+
+if [ $success -gt 0 -a $fixed -eq 0 -a $failed -eq 0 -a $skipped -eq 0 ]
+then
+ exit 0
+else
+ exit 1
+fi
--- /dev/null
+#!/usr/bin/env bash
+test_description='"notmuch compact"'
+. ./test-lib.sh
+
+add_message '[subject]=One'
+add_message '[subject]=Two'
+add_message '[subject]=Three'
+
+notmuch tag +tag1 \*
+notmuch tag +tag2 subject:Two
+notmuch tag -tag1 +tag3 subject:Three
+
+test_expect_success "Running compact" "notmuch compact --backup=${TEST_DIRECTORY}/xapian.old"
+
+test_begin_subtest "Compact preserves database"
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread)
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag2 unread)
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Three (inbox tag3 unread)"
+
+test_expect_success 'Restoring Backup' \
+ 'rm -Rf ${MAIL_DIR}/.notmuch/xapian &&
+ mv ${TEST_DIRECTORY}/xapian.old ${MAIL_DIR}/.notmuch/xapian'
+
+test_begin_subtest "Checking restored backup"
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread)
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag2 unread)
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Three (inbox tag3 unread)"
+
+test_done
"$((`notmuch search '*' | wc -l`))" \
"`notmuch count --output=threads '*'`"
+test_begin_subtest "files count"
+test_expect_equal \
+ "$((`notmuch search --output=files '*' | wc -l`))" \
+ "`notmuch count --output=files '*'`"
+
+test_begin_subtest "files count for a duplicate message-id"
+test_expect_equal \
+ "2" \
+ "`notmuch count --output=files id:20091117232137.GA7669@griffis1.net`"
+
test_begin_subtest "count with no matching messages"
test_expect_equal \
"0" \
elif (gpg --debug-quick-random --version >/dev/null 2>&1) ; then
echo debug-quick-random >> "$GNUPGHOME"/gpg.conf
fi
+ echo no-emit-version >> "$GNUPGHOME"/gpg.conf
}
##################################################
# get key fingerprint
FINGERPRINT=$(gpg --no-tty --list-secret-keys --with-colons --fingerprint | grep '^fpr:' | cut -d: -f10)
-# for some reason this is needed for emacs_deliver_message to work,
-# although I can't figure out why
-add_email_corpus
-
test_expect_success 'emacs delivery of signed message' \
-'emacs_deliver_message \
+'emacs_fcc_message \
"test signed message 001" \
"This is a test signed message." \
"(mml-secure-message-sign)"'
"content": "This is a test signed message.\n"},
{"id": 3,
"content-type": "application/pgp-signature",
- "content-length": 315}]}]},
+ "content-length": 280}]}]},
[]]]]'
test_expect_equal_json \
"$output" \
"content": "This is a test signed message.\n"},
{"id": 3,
"content-type": "application/pgp-signature",
- "content-length": 315}]}]},
+ "content-length": 280}]}]},
[]]]]'
test_expect_equal_json \
"$output" \
"content": "This is a test signed message.\n"},
{"id": 3,
"content-type": "application/pgp-signature",
- "content-length": 315}]}]},
+ "content-length": 280}]}]},
[]]]]'
test_expect_equal_json \
"$output" \
This is a test file.
EOF
test_expect_success 'emacs delivery of encrypted message with attachment' \
-'emacs_deliver_message \
+'emacs_fcc_message \
"test encrypted message 001" \
"This is a test encrypted message.\n" \
"(mml-attach-file \"TESTATTACHMENT\") (mml-secure-message-encrypt)"'
mv "${GNUPGHOME}"{.bak,}
test_expect_success 'emacs delivery of encrypted + signed message' \
-'emacs_deliver_message \
+'emacs_fcc_message \
"test encrypted message 002" \
"This is another test encrypted message.\n" \
"(mml-secure-message-sign-encrypt)"'
"content": "This is a test signed message.\n"},
{"id": 3,
"content-type": "application/pgp-signature",
- "content-length": 315}]}]},
+ "content-length": 280}]}]},
[]]]]'
test_expect_equal_json \
"$output" \
"[from]=\"\\\"Invalid \\\" From\\\" <test_suite@notmuchmail.org>\""
thread=$(notmuch search --output=threads subject:message-with-invalid-from)
test_emacs "(notmuch-show \"$thread\")
- (test-output)"
+ (test-output \"OUTPUT.raw\")"
cat <<EOF >EXPECTED
"Invalid " (2001-01-05) (inbox)
Subject: message-with-invalid-from
To: Notmuch Test Suite <test_suite@notmuchmail.org>
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
This is just a test message (#1)
EOF
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
test_expect_equal_file OUTPUT EXPECTED
test_begin_subtest "Navigation of notmuch-search to thread view"
output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)"
+test_begin_subtest "Add tag (large query)"
+# We use a long query to force us into batch mode and use a funny tag
+# that requires escaping for batch tagging.
+test_emacs "(notmuch-tag (concat \"$os_x_darwin_thread\" \" or \" (make-string notmuch-tag-argument-limit ?x)) (list \"+tag-from-%-large-query\"))"
+output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-%-large-query unread)"
+notmuch tag -tag-from-%-large-query $os_x_darwin_thread
+
test_begin_subtest "notmuch-show: add single tag to single message"
test_emacs "(notmuch-show \"$os_x_darwin_thread\")
(execute-kbd-macro \"+tag-from-show-view\")"
A: Top-posting.
Q: What is the most annoying thing in e-mail?"'
test_emacs "(notmuch-show \"top-posting\")
- (test-visible-output)"
+ (test-visible-output \"OUTPUT.raw\")"
echo "Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox)
Subject: The problem with top-posting
To: Notmuch Test Suite <test_suite@notmuchmail.org>
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
A: Because it messes up the order in which people normally read text.
Q: Why is top-posting such a bad thing?
Top Poster <top@poster.com> (2001-01-05) (inbox unread)
Subject: Re: The problem with top-posting
To: Notmuch Test Suite <test_suite@notmuchmail.org>
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
Thanks for the advice! I will be sure to put it to good use.
-Top Poster
[ 9-line hidden original message. Click/Enter to show. ]" > EXPECTED
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
test_expect_equal_file OUTPUT EXPECTED
test_begin_subtest "Hiding message in notmuch-show view"
test_begin_subtest "Do not call notmuch for non-inlinable application/mpeg parts"
id='message-with-application/mpeg-attachment@notmuchmail.org'
-emacs_deliver_message \
+emacs_fcc_message \
'Message with application/mpeg attachment' \
'' \
"(message-goto-eoh)
test_begin_subtest "Do not call notmuch for non-inlinable audio/mpeg parts"
id='message-with-audio/mpeg-attachment@notmuchmail.org'
-emacs_deliver_message \
+emacs_fcc_message \
'Message with audio/mpeg attachment' \
'' \
"(message-goto-eoh)
chmod a+x notmuch_fail
test_emacs "(let ((notmuch-command \"$PWD/notmuch_fail\"))
(with-current-buffer \"*Messages*\" (erase-buffer))
+ (with-current-buffer (get-buffer-create \"*Notmuch errors*\")
+ (erase-buffer))
(notmuch-search \"tag:inbox\")
(notmuch-test-wait)
(with-current-buffer \"*Messages*\"
(with-current-buffer \"*Notmuch errors*\"
(test-output \"ERROR\"))
(test-output))"
-sed -i -e 's/^\[.*\]$/[XXX]/' ERROR
-test_expect_equal "$(cat OUTPUT; echo ---; cat MESSAGES; echo ---; cat ERROR)" "\
+
+test_expect_equal "$(notmuch_emacs_error_sanitize notmuch_fail OUTPUT MESSAGES ERROR)" "\
+=== OUTPUT ===
End of search results.
----
-$PWD/notmuch_fail exited with status 1 (see *Notmuch errors* for more details)
----
+=== MESSAGES ===
+YYY/notmuch_fail exited with status 1 (see *Notmuch errors* for more details)
+=== ERROR ===
[XXX]
-$PWD/notmuch_fail exited with status 1
-command: $PWD/notmuch_fail search --format\=sexp --format-version\=1 --sort\=newest-first tag\:inbox
+YYY/notmuch_fail exited with status 1
+command: YYY/notmuch_fail search --format\=sexp --format-version\=2 --sort\=newest-first tag\:inbox
exit status: 1"
test_begin_subtest "Search handles subprocess warnings"
chmod a+x notmuch_fail
test_emacs "(let ((notmuch-command \"$PWD/notmuch_fail\"))
(with-current-buffer \"*Messages*\" (erase-buffer))
- (with-current-buffer \"*Notmuch errors*\" (erase-buffer))
+ (with-current-buffer (get-buffer-create \"*Notmuch errors*\")
+ (erase-buffer))
(notmuch-search \"tag:inbox\")
(notmuch-test-wait)
(with-current-buffer \"*Messages*\"
This is a warning
This is another warning"
+test_begin_subtest "Search thread tag operations are race-free"
+add_message '[subject]="Search race test"'
+gen_msg_id_1=$gen_msg_id
+generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \
+ '[references]="<'$gen_msg_id_1'>"' \
+ '[subject]="Search race test two"'
+test_emacs '(notmuch-search "subject:\"search race test\"")
+ (notmuch-test-wait)
+ (notmuch-poll)
+ (execute-kbd-macro "+search-thread-race-tag")'
+output=$(notmuch search --output=messages 'tag:search-thread-race-tag')
+test_expect_equal "$output" "id:$gen_msg_id_1"
+
+test_begin_subtest "Search global tag operations are race-free"
+generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \
+ '[references]="<'$gen_msg_id_1'>"' \
+ '[subject]="Re: Search race test"'
+test_emacs '(notmuch-search "subject:\"search race test\" -subject:two")
+ (notmuch-test-wait)
+ (notmuch-poll)
+ (execute-kbd-macro "*+search-global-race-tag")'
+output=$(notmuch search --output=messages 'tag:search-global-race-tag')
+test_expect_equal "$output" "id:$gen_msg_id_1"
+
test_done
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox)
Subject: Hiding Original Message region at beginning of a message
To: Notmuch Test Suite <test_suite@notmuchmail.org>
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
[ 2-line hidden original message. Click/Enter to show. ]
EOF
test_emacs "(notmuch-show \"id:$message_id\")
- (test-visible-output)"
+ (test-visible-output \"OUTPUT.raw\")"
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
test_expect_equal_file OUTPUT EXPECTED
test_begin_subtest "Bare subject #1"
test_emacs '(let ((notmuch-show-only-matching-messages nil))
(notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
(notmuch-test-wait)
- (let ((current-prefix-arg t))
- (notmuch-search-show-thread))
+ (notmuch-search-show-thread t)
(notmuch-test-wait)
(test-visible-output))'
test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-elide-non-matching-messages-on
mid:abc. mid:abc, mid:abc;"'
test_emacs '(notmuch-show "id:'$gen_msg_id'")
(notmuch-test-mark-links)
- (test-visible-output)'
+ (test-visible-output "OUTPUT.raw")'
cat <<EOF >EXPECTED
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox)
Subject: id buttonization
To: Notmuch Test Suite <test_suite@notmuchmail.org>
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
<<id:abc>>
<<id:abc.def>>. <<id:abc,def>>, <<id:abc;def>>; <<id:abc:def>>:
<<mid:abc%20def>>
<<mid:abc>>. <<mid:abc>>, <<mid:abc>>;
EOF
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
test_expect_equal_file OUTPUT EXPECTED
(with-current-buffer \"*Notmuch errors*\"
(test-output \"ERROR\"))
(test-output))"
-sed -i -e 's/^\[.*\]$/[XXX]/' ERROR
-test_expect_equal "$(cat OUTPUT; echo ---; cat MESSAGES; echo ---; cat ERROR)" "\
----
+test_expect_equal "$(notmuch_emacs_error_sanitize notmuch_fail OUTPUT MESSAGES ERROR)" "\
+=== OUTPUT ===
+=== MESSAGES ===
This is an error (see *Notmuch errors* for more details)
----
+=== ERROR ===
[XXX]
This is an error
-command: $PWD/notmuch_fail show --format\\=sexp --format-version\\=1 --exclude\\=false \\' \\* \\'
+command: YYY/notmuch_fail show --format\\=sexp --format-version\\=1 --exclude\\=false \\' \\* \\'
exit status: 1
stderr:
This is an error
--- /dev/null
+#!/usr/bin/env bash
+
+test_description="emacs tree view interface"
+. test-lib.sh
+
+EXPECTED=$TEST_DIRECTORY/tree.expected-output
+
+add_email_corpus
+
+test_begin_subtest "Basic notmuch-tree view in emacs"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-tag-inbox
+
+test_begin_subtest "Refreshed notmuch-tree view in emacs"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ (notmuch-tree-refresh-view)
+ (notmuch-test-wait)
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-tag-inbox
+
+# In the following tag tests we make sure the display is updated
+# correctly and, in a separate test, that the database is updated
+# correctly.
+
+test_begin_subtest "Tag message in notmuch tree view (display)"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ (forward-line)
+ (notmuch-tree-tag (list "+test_tag"))
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-tag-inbox-tagged
+
+test_begin_subtest "Tag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_tag')
+test_expect_equal "$output" "id:877h1wv7mg.fsf@inf-8657.int-evry.fr"
+
+test_begin_subtest "Untag message in notmuch tree view"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ (forward-line)
+ (notmuch-tree-tag (list "-test_tag"))
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-tag-inbox
+
+test_begin_subtest "Untag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_tag')
+test_expect_equal "$output" ""
+
+test_begin_subtest "Tag thread in notmuch tree view"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ ;; move to a sizable thread
+ (forward-line 26)
+ (notmuch-tree-tag-thread (list "+test_thread_tag"))
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-tag-inbox-thread-tagged
+
+test_begin_subtest "Tag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_thread_tag')
+test_expect_equal "$output" \
+"id:87ocn0qh6d.fsf@yoom.home.cworth.org
+id:20091118005040.GA25380@dottiness.seas.harvard.edu
+id:yunaayketfm.fsf@aiko.keithp.com
+id:87fx8can9z.fsf@vertex.dottedmag
+id:20091117203301.GV3165@dottiness.seas.harvard.edu
+id:87iqd9rn3l.fsf@vertex.dottedmag
+id:20091117190054.GU3165@dottiness.seas.harvard.edu"
+
+test_begin_subtest "Untag thread in notmuch tree view"
+test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ ;; move to the same sizable thread as above
+ (forward-line 26)
+ (notmuch-tree-tag-thread (list "-test_thread_tag"))
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-tag-inbox
+
+test_begin_subtest "Untag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_thread_tag')
+test_expect_equal "$output" ""
+
+test_begin_subtest "Navigation of notmuch-hello to search results"
+test_emacs '(notmuch-hello)
+ (goto-char (point-min))
+ (re-search-forward "inbox")
+ (widget-button-press (1- (point)))
+ (notmuch-test-wait)
+ (notmuch-tree-from-search-current-query)
+ (notmuch-test-wait)
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-tag-inbox
+
+test_begin_subtest "Tree view of a single thread (from search)"
+test_emacs '(notmuch-hello)
+ (goto-char (point-min))
+ (re-search-forward "inbox")
+ (widget-button-press (1- (point)))
+ (notmuch-test-wait)
+ (notmuch-tree-from-search-thread)
+ (notmuch-test-wait)
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-single-thread
+
+test_begin_subtest "Tree view of a single thread (from show)"
+test_emacs '(notmuch-hello)
+ (goto-char (point-min))
+ (re-search-forward "inbox")
+ (widget-button-press (1- (point)))
+ (notmuch-test-wait)
+ (notmuch-search-show-thread)
+ (notmuch-tree-from-show-current-query)
+ (notmuch-test-wait)
+ (test-output)
+ (delete-other-windows)'
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-single-thread
+
+test_begin_subtest "Message window of tree view"
+test_emacs '(notmuch-hello)
+ (goto-char (point-min))
+ (re-search-forward "inbox")
+ (widget-button-press (1- (point)))
+ (notmuch-test-wait)
+ (notmuch-search-next-thread)
+ (notmuch-tree-from-search-thread)
+ (notmuch-test-wait)
+ (select-window notmuch-tree-message-window)
+ (test-output)
+ (delete-other-windows)'
+cp OUTPUT /tmp/mjwout
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-tree-show-window
+
+test_begin_subtest "Stash id"
+output=$(test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (require (quote notmuch-tree))
+ (notmuch-tree "id:1258498485-sup-142@elly")
+ (notmuch-test-wait)
+ (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:1258498485-sup-142@elly\""
+
+test_begin_subtest "Move to next matching message"
+output=$(test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (require (quote notmuch-tree))
+ (notmuch-tree "from:cworth")
+ (notmuch-test-wait)
+ (notmuch-tree-next-matching-message)
+ (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:878we4qdqf.fsf@yoom.home.cworth.org\""
+
+test_begin_subtest "Move to next thread"
+output=$(test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (require (quote notmuch-tree))
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ (forward-line 26)
+ (notmuch-tree-next-thread)
+ (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net\""
+
+test_begin_subtest "Move to previous thread"
+output=$(test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (require (quote notmuch-tree))
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ (forward-line 26)
+ (notmuch-tree-prev-thread)
+ (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:20091117190054.GU3165@dottiness.seas.harvard.edu\""
+
+test_begin_subtest "Move to previous previous thread"
+output=$(test_emacs '(add-to-list (quote load-path) "'$PICK_DIR'")
+ (require (quote notmuch-tree))
+ (notmuch-tree "tag:inbox")
+ (notmuch-test-wait)
+ (forward-line 26)
+ (notmuch-tree-prev-thread)
+ (notmuch-tree-prev-thread)
+ (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:1258493565-13508-1-git-send-email-keithp@keithp.com\""
+
+test_done
test_begin_subtest "Message with text of unknown charset"
add_message '[content-type]="text/plain; charset=unknown-8bit"' \
"[body]=irrelevant"
-output=$(notmuch show id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
-test_expect_equal "$output" "\fmessage{ id:msg-001@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-001
+output=$(notmuch show id:${gen_msg_id} 2>&1 | notmuch_show_sanitize_all)
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
\fheader{
Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox unread)
Subject: Message with text of unknown charset
From: Notmuch Test Suite <test_suite@notmuchmail.org>
To: Notmuch Test Suite <test_suite@notmuchmail.org>
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
\fheader}
\fbody{
\fpart{ ID: 1, Content-type: text/plain
'[content-transfer-encoding]=8bit' \
'[subject]="ISO-8859-2 encoded message"' \
"[body]=$'Czech word tu\350\362\341\350\350\355 means pinguin\'s.'" # ISO-8859-2 characters are generated by shell's escape sequences
-output=$(notmuch search tučňáččí 2>&1 | notmuch_show_sanitize)
+output=$(notmuch search tučňáččí 2>&1 | notmuch_show_sanitize_all)
test_expect_equal "$output" "thread:0000000000000002 2001-01-05 [1/1] Notmuch Test Suite; ISO-8859-2 encoded message (inbox unread)"
+test_begin_subtest "RFC 2047 encoded word with spaces"
+add_message '[subject]="=?utf-8?q?encoded word with spaces?="'
+output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
+test_expect_equal "$output" "thread:0000000000000003 2001-01-05 [1/1] Notmuch Test Suite; encoded word with spaces (inbox unread)"
+
+test_begin_subtest "RFC 2047 encoded words back to back"
+add_message '[subject]="=?utf-8?q?encoded-words-back?==?utf-8?q?to-back?="'
+output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
+test_expect_equal "$output" "thread:0000000000000004 2001-01-05 [1/1] Notmuch Test Suite; encoded-words-backto-back (inbox unread)"
+
+test_begin_subtest "RFC 2047 encoded words without space before or after"
+add_message '[subject]="=?utf-8?q?encoded?=word without=?utf-8?q?space?=" '
+output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
+test_expect_equal "$output" "thread:0000000000000005 2001-01-05 [1/1] Notmuch Test Suite; encodedword withoutspace (inbox unread)"
+
test_done
test_begin_subtest "Search, don't exclude \"deleted\" messages when --exclude=flag specified"
output=$(notmuch search --exclude=flag subject:deleted | notmuch_search_sanitize)
test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
-thread:XXX 2001-01-05 [1/2] Notmuch Test Suite; Not deleted reply (deleted inbox unread)"
+thread:XXX 2001-01-05 [1/2] Notmuch Test Suite; Deleted (deleted inbox unread)"
test_begin_subtest "Search, don't exclude \"deleted\" messages from search if not configured"
notmuch config set search.exclude_tags
test_begin_subtest "Search, exclude=flag (thread summary)"
output=$(notmuch search --exclude=flag tag:test | notmuch_search_sanitize)
test_expect_equal "$output" "thread:XXX 2001-01-05 [0/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
-thread:XXX 2001-01-05 [0/6] Notmuch Test Suite; All messages excluded: double match: reply 4 (deleted inbox test unread)
+thread:XXX 2001-01-05 [0/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
thread:XXX 2001-01-05 [0/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)
thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
thread:XXX 2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
test_begin_subtest "Insert message, add tag"
gen_insert_msg
notmuch insert +custom < "$gen_msg_filename"
-output=$(notmuch count tag:custom)
-test_expect_equal "$output" "1"
+output=$(notmuch search --output=messages tag:custom)
+test_expect_equal "$output" "id:$gen_msg_id"
test_begin_subtest "Insert message, add/remove tags"
gen_insert_msg
notmuch insert +custom -unread < "$gen_msg_filename"
-output=$(notmuch count tag:custom NOT tag:unread)
-test_expect_equal "$output" "1"
+output=$(notmuch search --output=messages tag:custom NOT tag:unread)
+test_expect_equal "$output" "id:$gen_msg_id"
+
+test_begin_subtest "Insert message with default tags stays in new/"
+gen_insert_msg
+notmuch insert < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/new"
+
+test_begin_subtest "Insert message with non-maildir synced tags stays in new/"
+gen_insert_msg
+notmuch insert +custom -inbox < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/new"
+
+test_begin_subtest "Insert message with custom new.tags goes to cur/"
+OLDCONFIG=$(notmuch config get new.tags)
+notmuch config set new.tags test
+gen_insert_msg
+notmuch insert < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+notmuch config set new.tags $OLDCONFIG
+test_expect_equal "$dirname" "$MAIL_DIR/cur"
+
+# additional check on the previous message
+test_begin_subtest "Insert message with custom new.tags actually gets the tags"
+output=$(notmuch search --output=tags id:$gen_msg_id)
+test_expect_equal "$output" "test"
+
+test_begin_subtest "Insert message with maildir synced tags goes to cur/"
+gen_insert_msg
+notmuch insert +flagged < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/cur"
test_begin_subtest "Insert message into folder"
gen_insert_msg
notmuch insert --folder=Drafts < "$gen_msg_filename"
output=$(notmuch search --output=files folder:Drafts)
dirname=$(dirname "$output")
-test_expect_equal "$dirname" "$MAIL_DIR/Drafts/cur"
+test_expect_equal "$dirname" "$MAIL_DIR/Drafts/new"
test_begin_subtest "Insert message into folder, add/remove tags"
gen_insert_msg
notmuch insert --folder=Drafts +draft -unread < "$gen_msg_filename"
-output=$(notmuch count folder:Drafts tag:draft NOT tag:unread)
-test_expect_equal "$output" "1"
+output=$(notmuch search --output=messages folder:Drafts tag:draft NOT tag:unread)
+test_expect_equal "$output" "id:$gen_msg_id"
gen_insert_msg
test_expect_code 1 "Insert message into non-existent folder" \
notmuch insert --folder=F --create-folder +folder < "$gen_msg_filename"
output=$(notmuch search --output=files folder:F tag:folder)
basename=$(basename "$output")
-test_expect_equal_file "$gen_msg_filename" "$MAIL_DIR/F/cur/${basename}"
+test_expect_equal_file "$gen_msg_filename" "$MAIL_DIR/F/new/${basename}"
test_begin_subtest "Insert message, create subfolder"
gen_insert_msg
notmuch insert --folder=F/G/H/I/J --create-folder +folder < "$gen_msg_filename"
output=$(notmuch search --output=files folder:F/G/H/I/J tag:folder)
basename=$(basename "$output")
-test_expect_equal_file "$gen_msg_filename" "${MAIL_DIR}/F/G/H/I/J/cur/${basename}"
+test_expect_equal_file "$gen_msg_filename" "${MAIL_DIR}/F/G/H/I/J/new/${basename}"
test_begin_subtest "Insert message, create existing subfolder"
gen_insert_msg
\"total\": 1,
\"authors\": \"Notmuch Test Suite\",
\"subject\": \"json-search-subject\",
+ \"query\": [\"id:$gen_msg_id\", null],
\"tags\": [\"inbox\",
\"unread\"]}]"
test_begin_subtest "Show message: json, inline attachment filename"
subject='json-show-inline-attachment-filename'
id="json-show-inline-attachment-filename@notmuchmail.org"
-emacs_deliver_message \
+emacs_fcc_message \
"$subject" \
'This is a test message with inline attachment with a filename' \
"(mml-attach-file \"$TEST_DIRECTORY/README\" nil nil \"inline\")
\"total\": 1,
\"authors\": \"Notmuch Test Suite\",
\"subject\": \"json-search-utf8-body-sübjéct\",
+ \"query\": [\"id:$gen_msg_id\", null],
\"tags\": [\"inbox\",
\"unread\"]}]"
. ./test-lib.sh
-# Avoid including the local value of MAIL_DIR in the result.
-filter_show_json() {
- sed -e "s|${MAIL_DIR}/|MAIL_DIR/|"
-}
-
# Create the expected maildir structure
mkdir $MAIL_DIR/cur
mkdir $MAIL_DIR/new
test_expect_equal "$output" "adding-replied-tag:2,RS"
test_begin_subtest "notmuch show works with renamed file (without notmuch new)"
-output=$(notmuch show --format=json id:${gen_msg_id} | filter_show_json)
-test_expect_equal_json "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
+output=$(notmuch show --format=json id:${gen_msg_id} | notmuch_json_show_sanitize)
+test_expect_equal_json "$output" '[[[{"id": "XXXXX",
"match": true,
"excluded": false,
-"filename": "MAIL_DIR/cur/adding-replied-tag:2,RS",
-"timestamp": 978709437,
+"filename": "YYYYY",
+"timestamp": 42,
"date_relative": "2001-01-05",
"tags": ["inbox","replied"],
"headers": {"Subject": "Adding replied tag",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
-"Date": "Fri, 05 Jan 2001 15:43:57 +0000"},
+"Date": "GENERATED_DATE"},
"body": [{"id": 1,
"content-type": "text/plain",
"content": "This is just a test message (#3)\n"}]},
# creating new directories in the mail store, then it should be
# creating all necessary database state for those directories.
+test_begin_subtest "Adding non-maildir tags does not move message from new to cur"
+add_message [subject]='"Message to stay in new"' \
+ [date]='"Sat, 01 Jan 2000 12:00:00 -0000"' \
+ [filename]='message-to-stay-in-new' [dir]=new
+notmuch tag +donotmove subject:"Message to stay in new"
+output=$(cd "$MAIL_DIR"; ls */message-to-stay-in-new*)
+test_expect_equal "$output" "new/message-to-stay-in-new"
+
+test_begin_subtest "Message in cur lacking maildir info gets one on any tag change"
+add_message [filename]='message-to-get-maildir-info' [dir]=cur
+notmuch tag +anytag id:$gen_msg_id
+output=$(cd "$MAIL_DIR"; ls */message-to-get-maildir-info*)
+test_expect_equal "$output" "cur/message-to-get-maildir-info:2,"
+
+test_begin_subtest "Message in new with maildir info is moved to cur on any tag change"
+add_message [filename]='message-with-info-to-be-moved-to-cur:2,' [dir]=new
+notmuch tag +anytag id:$gen_msg_id
+output=$(cd "$MAIL_DIR"; ls */message-with-info-to-be-moved-to-cur*)
+test_expect_equal "$output" "cur/message-with-info-to-be-moved-to-cur:2,"
+
test_begin_subtest "Removing 'S' flag from existing filename adds 'unread' tag"
add_message [subject]='"Removing S flag"' [filename]='removing-s-flag:2,S' [dir]=cur
output=$(notmuch search subject:"Removing S flag" | notmuch_search_sanitize)
],
"thread": "XXX",
"timestamp": 978709437,
- "total": 1
+ "total": 1,
+ "query": ["id:notmuch-sha1-7a6e4eac383ef958fcd3ebf2143db71b8ff01161", null]
},
{
"authors": "Notmuch Test Suite",
],
"thread": "XXX",
"timestamp": 0,
- "total": 1
+ "total": 1,
+ "query": ["id:notmuch-sha1-ca55943aff7a72baf2ab21fa74fab3d632401334", null]
}
]'
test_begin_subtest "Show: json"
output=$(notmuch show --format=json '*' | notmuch_json_show_sanitize)
-test_expect_equal_json "$output" '
+expected=$(notmuch_json_show_sanitize <<EOF
[
[
[
[]
]
]
-]'
-
+]
+EOF
+)
+test_expect_equal_json "$output" "$expected"
test_done
test_begin_subtest "'notmuch reply' to a multipart message with json format"
notmuch reply --format=json 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT
-cat <<EOF >EXPECTED
+notmuch_json_show_sanitize <<EOF >EXPECTED
{"reply-headers": {"Subject": "Re: Multipart message",
"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
"To": "Carl Worth <cworth@cworth.org>, cworth@cworth.org",
"In-reply-to": "<87liy5ap00.fsf@yoom.home.cworth.org>",
- "References": " <87liy5ap00.fsf@yoom.home.cworth.org>"},
+ "References": "<87liy5ap00.fsf@yoom.home.cworth.org>"},
"original": {"id": "XXXXX",
"match": false,
"excluded": false,
echo -n -e "\xEF\x0D\x0A" > crlf.expected
test_expect_equal_file crlf.out crlf.expected
-test_done
\ No newline at end of file
+
+# The ISO-8859-1 encoding of U+00BD is a single byte: octal 275
+# (Portability note: Dollar-Single ($'...', ANSI C-style escape sequences)
+# quoting works on bash, ksh, zsh, *BSD sh but not on dash, ash nor busybox sh)
+readonly u_00bd_latin1=$'\275'
+
+# The Unicode fraction symbol 1/2 is U+00BD and is encoded
+# in UTF-8 as two bytes: octal 302 275
+readonly u_00bd_utf8=$'\302\275'
+
+cat <<EOF > ${MAIL_DIR}/include-html
+From: A <a@example.com>
+To: B <b@example.com>
+Subject: html message
+Date: Sat, 01 January 2000 00:00:00 +0000
+Message-ID: <htmlmessage>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=="
+
+--==-==
+Content-Type: text/html; charset=UTF-8
+
+<p>0.5 equals ${u_00bd_utf8}</p>
+
+--==-==
+Content-Type: text/html; charset=ISO-8859-1
+
+<p>0.5 equals ${u_00bd_latin1}</p>
+
+--==-==
+Content-Type: text/plain; charset=UTF-8
+
+0.5 equals ${u_00bd_utf8}
+
+--==-==--
+EOF
+
+notmuch new > /dev/null
+
+cat_expected_head ()
+{
+ cat <<EOF
+[[[{"id": "htmlmessage", "match":true, "excluded": false, "date_relative":"2000-01-01",
+ "timestamp": 946684800,
+ "filename": "${MAIL_DIR}/include-html",
+ "tags": ["inbox", "unread"],
+ "headers": { "Date": "Sat, 01 Jan 2000 00:00:00 +0000", "From": "A <a@example.com>",
+ "Subject": "html message", "To": "B <b@example.com>"},
+ "body": [{
+ "content-type": "multipart/alternative", "id": 1,
+EOF
+}
+
+cat_expected_head > EXPECTED.nohtml
+cat <<EOF >> EXPECTED.nohtml
+"content": [
+ { "id": 2, "content-charset": "UTF-8", "content-length": 21, "content-type": "text/html"},
+ { "id": 3, "content-charset": "ISO-8859-1", "content-length": 20, "content-type": "text/html"},
+ { "id": 4, "content-type": "text/plain", "content": "0.5 equals \\u00bd\\n"}
+]}]},[]]]]
+EOF
+
+# Both the UTF-8 and ISO-8859-1 part should have U+00BD
+cat_expected_head > EXPECTED.withhtml
+cat <<EOF >> EXPECTED.withhtml
+"content": [
+ { "id": 2, "content-type": "text/html", "content": "<p>0.5 equals \\u00bd</p>\\n"},
+ { "id": 3, "content-type": "text/html", "content": "<p>0.5 equals \\u00bd</p>\\n"},
+ { "id": 4, "content-type": "text/plain", "content": "0.5 equals \\u00bd\\n"}
+]}]},[]]]]
+EOF
+
+test_begin_subtest "html parts excluded by default"
+notmuch show --format=json id:htmlmessage > OUTPUT
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED.nohtml)"
+
+test_begin_subtest "html parts included"
+notmuch show --format=json --include-html id:htmlmessage > OUTPUT
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED.withhtml)"
+
+test_done
test_begin_subtest "Ignore files and directories specified in new.ignore (multiple occurrences)"
notmuch config set new.ignore .git ignored_file .ignored_hidden_file
+notmuch new > /dev/null # ensure that files/folders will be printed in ASCII order.
touch "${MAIL_DIR}"/.git # change .git's mtime for notmuch new to rescan.
+touch "${MAIL_DIR}" # likewise for MAIL_DIR
mkdir -p "${MAIL_DIR}"/one/two/three/.git
-notmuch new > /dev/null # ensure that files/folders will be printed in ASCII order.
touch "${MAIL_DIR}"/{one,one/two,one/two/three}/ignored_file
output=$(NOTMUCH_NEW --debug 2>&1 | sort)
test_expect_equal "$output" \
TESTS="
basic
help-test
+ compact
config
setup
new
emacs-address-cleaning
emacs-hello
emacs-show
+ emacs-tree
missing-headers
hex-escaping
parse-time-string
# Report results
./aggregate-results.sh test-results/*
+ev=$?
# Clean up
rm -rf test-results corpus.mail
+
+exit $ev
test_expect_equal "$output" "Error: search term did not match precisely one message."
test_begin_subtest "Show a raw message"
-output=$(notmuch show --format=raw id:msg-001@notmuch-test-suite)
+output=$(notmuch show --format=raw id:msg-001@notmuch-test-suite | notmuch_date_sanitize)
test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
To: Notmuch Test Suite <test_suite@notmuchmail.org>
Message-Id: <msg-001@notmuch-test-suite>
Subject: Test message #1
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
This is just a test message (#1)"
test_begin_subtest "Show another raw message"
-output=$(notmuch show --format=raw id:msg-002@notmuch-test-suite)
+output=$(notmuch show --format=raw id:msg-002@notmuch-test-suite | notmuch_date_sanitize)
test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
To: Notmuch Test Suite <test_suite@notmuchmail.org>
Message-Id: <msg-002@notmuch-test-suite>
Subject: Test message #2
-Date: Fri, 05 Jan 2001 15:43:57 +0000
+Date: GENERATED_DATE
This is just a test message (#2)"
'[body]="200-byte header"'
output=$(notmuch reply id:${gen_msg_id})
test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
-Subject: Re: This subject is exactly 200 bytes in length. Other than its length there is not much of note here. Note that the length of 200 bytes includes the Subject: and Re: prefixes with two spaces
+Subject: Re: This subject is exactly 200 bytes in length. Other than its
+ length there is not much of note here. Note that the length of 200 bytes
+ includes the Subject: and Re: prefixes with two spaces
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
> From guessing"
+test_begin_subtest "Reply with RFC 2047-encoded headers"
+add_message '[subject]="=?iso-8859-1?q?=e0=df=e7?="' \
+ '[from]="=?utf-8?q?=e2=98=83?= <snowman@example.com>"' \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+ '[body]="Encoding"'
+
+# GMime happens to change from Q- to B-encoding. We canonicalize the
+# case of the encoding and charset because different versions of GMime
+# capitalize the encoding differently.
+output=$(notmuch reply id:${gen_msg_id} | perl -pe 's/=\?[^?]+\?[bB]\?/lc($&)/ge')
+test_expect_equal "$output" "\
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: =?iso-8859-1?b?4N/n?=
+To: =?utf-8?b?4piD?= <snowman@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, ☃ <snowman@example.com> wrote:
+> Encoding"
+
+test_begin_subtest "Reply with RFC 2047-encoded headers (JSON)"
+output=$(notmuch reply --format=json id:${gen_msg_id})
+test_expect_equal_json "$output" '
+{
+ "original": {
+ "body": [
+ {
+ "content": "Encoding\n",
+ "content-type": "text/plain",
+ "id": 1
+ }
+ ],
+ "date_relative": "2010-01-05",
+ "excluded": false,
+ "filename": "'${MAIL_DIR}'/msg-012",
+ "headers": {
+ "Date": "Tue, 05 Jan 2010 15:43:56 +0000",
+ "From": "\u2603 <snowman@example.com>",
+ "Subject": "\u00e0\u00df\u00e7",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>"
+ },
+ "id": "'${gen_msg_id}'",
+ "match": false,
+ "tags": [
+ "inbox",
+ "unread"
+ ],
+ "timestamp": 1262706236
+ },
+ "reply-headers": {
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "In-reply-to": "<'${gen_msg_id}'>",
+ "References": "<'${gen_msg_id}'>",
+ "Subject": "Re: \u00e0\u00df\u00e7",
+ "To": "\u2603 <snowman@example.com>"
+ }
+}'
+
+
test_done
'[body]="200-byte header"'
output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
-Subject: Re: This subject is exactly 200 bytes in length. Other than its length there is not much of note here. Note that the length of 200 bytes includes the Subject: and Re: prefixes with two spaces
+Subject: Re: This subject is exactly 200 bytes in length. Other than its
+ length there is not much of note here. Note that the length of 200 bytes
+ includes the Subject: and Re: prefixes with two spaces
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
EOF
test_expect_equal_file OUTPUT EXPECTED
+test_begin_subtest "--output=files --duplicate=1"
+notmuch search --output=files --duplicate=1 '*' | sed -e "s,$MAIL_DIR,MAIL_DIR," >OUTPUT
+cat <<EOF >EXPECTED
+MAIL_DIR/cur/52:2,
+MAIL_DIR/cur/53:2,
+MAIL_DIR/cur/50:2,
+MAIL_DIR/cur/49:2,
+MAIL_DIR/cur/48:2,
+MAIL_DIR/cur/47:2,
+MAIL_DIR/cur/46:2,
+MAIL_DIR/cur/45:2,
+MAIL_DIR/cur/44:2,
+MAIL_DIR/cur/43:2,
+MAIL_DIR/cur/42:2,
+MAIL_DIR/cur/41:2,
+MAIL_DIR/cur/40:2,
+MAIL_DIR/cur/39:2,
+MAIL_DIR/cur/38:2,
+MAIL_DIR/cur/37:2,
+MAIL_DIR/cur/36:2,
+MAIL_DIR/cur/35:2,
+MAIL_DIR/cur/34:2,
+MAIL_DIR/cur/33:2,
+MAIL_DIR/cur/32:2,
+MAIL_DIR/cur/31:2,
+MAIL_DIR/cur/30:2,
+MAIL_DIR/cur/29:2,
+MAIL_DIR/cur/28:2,
+MAIL_DIR/cur/27:2,
+MAIL_DIR/cur/26:2,
+MAIL_DIR/cur/25:2,
+MAIL_DIR/cur/24:2,
+MAIL_DIR/cur/23:2,
+MAIL_DIR/cur/22:2,
+MAIL_DIR/cur/21:2,
+MAIL_DIR/cur/19:2,
+MAIL_DIR/cur/18:2,
+MAIL_DIR/cur/20:2,
+MAIL_DIR/cur/17:2,
+MAIL_DIR/cur/16:2,
+MAIL_DIR/cur/15:2,
+MAIL_DIR/cur/14:2,
+MAIL_DIR/cur/13:2,
+MAIL_DIR/cur/12:2,
+MAIL_DIR/cur/11:2,
+MAIL_DIR/cur/10:2,
+MAIL_DIR/cur/09:2,
+MAIL_DIR/cur/08:2,
+MAIL_DIR/cur/06:2,
+MAIL_DIR/cur/05:2,
+MAIL_DIR/cur/04:2,
+MAIL_DIR/cur/03:2,
+MAIL_DIR/cur/07:2,
+MAIL_DIR/cur/02:2,
+MAIL_DIR/cur/01:2,
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
test_begin_subtest "--output=files --format=json"
notmuch search --format=json --output=files '*' | sed -e "s,$MAIL_DIR,MAIL_DIR," >OUTPUT
cat <<EOF >EXPECTED
EOF
test_expect_equal_file OUTPUT EXPECTED
+test_begin_subtest "--output=files --format=json --duplicate=2"
+notmuch search --format=json --output=files --duplicate=2 '*' | sed -e "s,$MAIL_DIR,MAIL_DIR," >OUTPUT
+cat <<EOF >EXPECTED
+["MAIL_DIR/cur/51:2,"]
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
test_begin_subtest "--output=tags"
notmuch search --output=tags '*' >OUTPUT
cat <<EOF >EXPECTED
test_begin_subtest "Search message: sexp"
add_message "[subject]=\"sexp-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"sexp-search-message\""
output=$(notmuch search --format=sexp "sexp-search-message" | notmuch_search_sanitize)
-test_expect_equal "$output" "((:thread \"0000000000000002\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-subject\" :tags (\"inbox\" \"unread\")))"
+test_expect_equal "$output" "((:thread \"0000000000000002\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-subject\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))"
test_begin_subtest "Show message: sexp, utf-8"
add_message "[subject]=\"sexp-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
test_begin_subtest "Show message: sexp, inline attachment filename"
subject='sexp-show-inline-attachment-filename'
id="sexp-show-inline-attachment-filename@notmuchmail.org"
-emacs_deliver_message \
+emacs_fcc_message \
"$subject" \
'This is a test message with inline attachment with a filename' \
"(mml-attach-file \"$TEST_DIRECTORY/README\" nil nil \"inline\")
test_begin_subtest "Search message: sexp, utf-8"
add_message "[subject]=\"sexp-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
output=$(notmuch search --format=sexp "jsön-search-méssage" | notmuch_search_sanitize)
-test_expect_equal "$output" "((:thread \"0000000000000005\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :tags (\"inbox\" \"unread\")))"
+test_expect_equal "$output" "((:thread \"0000000000000005\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))"
test_done
(setq start next-pos)))
str))
+;; process-attributes is not defined everywhere, so define an
+;; alternate way to test if a process still exists.
+
+(defun test-process-running (pid)
+ (= 0
+ (signal-process pid 0)))
+
(defun orphan-watchdog-check (pid)
"Periodically check that the process with id PID is still
running, quit if it terminated."
- (if (not (process-attributes pid))
+ (if (not (test-process-running pid))
(kill-emacs)))
(defun orphan-watchdog (pid)
"Initiate orphan watchdog check."
- ; If process-attributes returns nil right away, that probably means
- ; it is unimplimented. So we delay two minutes before killing emacs.
- (if (process-attributes pid)
- (run-at-time 60 60 'orphan-watchdog-check pid)
- (run-at-time 120 60 'orphan-watchdog-check pid)))
+ (run-at-time 60 60 'orphan-watchdog-check pid))
(defun hook-counter (hook)
"Count how many times a hook is called. Increments
exit 1
fi
+# Make sure echo builtin does not expand backslash-escape sequences by default.
+shopt -u xpg_echo
+
# if --tee was passed, write the output not only to the terminal, but
# additionally to the file test-results/$BASENAME.out, too.
case "$GIT_TEST_TEE_STARTED, $* " in
fi
if [ -z "${template[date]}" ]; then
- template[date]="Fri, 05 Jan 2001 15:43:57 +0000"
+ # we use decreasing timestamps here for historical reasons;
+ # the existing test suite when we converted to unique timestamps just
+ # happened to have signicantly fewer failures with that choice.
+ template[date]=$(TZ=UTC printf "%(%a, %d %b %Y %T %z)T\n" \
+ $((978709437 - gen_msg_cnt)))
fi
additional_headers=""
test_emacs \
"(let ((message-send-mail-function 'message-smtpmail-send-it)
+ (mail-host-address \"example.com\")
(smtpmail-smtp-server \"localhost\")
(smtpmail-smtp-service \"25025\"))
- (notmuch-hello)
(notmuch-mua-mail)
(message-goto-to)
(insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\")
notmuch new >/dev/null
}
+# Pretend to deliver a message with emacs. Really save it to a file
+# and add it to the database
+#
+# Uses emacs to generate and deliver a message to the mail store.
+# Accepts arbitrary extra emacs/elisp functions to modify the message
+# before sending, which is useful to doing things like attaching files
+# to the message and encrypting/signing.
+emacs_fcc_message ()
+{
+ local subject="$1"
+ local body="$2"
+ shift 2
+ # before we can send a message, we have to prepare the FCC maildir
+ mkdir -p "$MAIL_DIR"/sent/{cur,new,tmp}
+
+ test_emacs \
+ "(let ((message-send-mail-function (lambda () t))
+ (mail-host-address \"example.com\"))
+ (notmuch-mua-mail)
+ (message-goto-to)
+ (insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\")
+ (message-goto-subject)
+ (insert \"${subject}\")
+ (message-goto-body)
+ (insert \"${body}\")
+ $@
+ (message-send-and-exit))" || return 1
+ notmuch new >/dev/null
+}
+
# Generate a corpus of email and add it to the database.
#
# This corpus is fixed, (it happens to be 50 messages from early in
{
sed \
-e 's| filename:.*| filename:XXXXX|' \
- -e 's| id:[^ ]* | id:XXXXX |'
+ -e 's| id:[^ ]* | id:XXXXX |' | \
+ notmuch_date_sanitize
}
notmuch_json_show_sanitize ()
{
sed \
-e 's|"id": "[^"]*",|"id": "XXXXX",|g' \
- -e 's|"filename": "/[^"]*",|"filename": "YYYYY",|g'
+ -e 's|"Date": "Fri, 05 Jan 2001 [^"]*0000"|"Date": "GENERATED_DATE"|g' \
+ -e 's|"filename": "/[^"]*",|"filename": "YYYYY",|g' \
+ -e 's|"timestamp": 97.......|"timestamp": 42|g'
+}
+
+notmuch_emacs_error_sanitize ()
+{
+ local command=$1
+ shift
+ for file in "$@"; do
+ echo "=== $file ==="
+ cat "$file"
+ done | sed \
+ -e 's/^\[.*\]$/[XXX]/' \
+ -e "s|^\(command: \)\{0,1\}/.*/$command|\1YYY/$command|"
}
+notmuch_date_sanitize ()
+{
+ sed \
+ -e 's/^Date: Fri, 05 Jan 2001 .*0000/Date: GENERATED_DATE/'
+}
# End of notmuch helper functions
# Use test_set_prereq to tell that a particular prerequisite is available.
--- /dev/null
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+Subject: [notmuch] Working with Maildir storage?
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 14:00:54 -0500
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ text/plain ]
+I saw the LWN article and decided to take a look at notmuch. I'm
+currently using mutt and mairix to index and read a collection of
+Maildir mail folders (around 40,000 messages total).
+
+notmuch indexed the messages without complaint, but my attempt at
+searching bombed out. Running, for example:
+
+ notmuch search storage
+
+Resulted in 4604 lines of errors along the lines of:
+
+ Error opening
+ /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+ Too many open files
+
+I'm curious if this is expected behavior (i.e., notmuch does not work
+with Maildir) or if something else is going on.
+
+Cheers,
+
+[ 4-line signature. Click/Enter to show. ]
+--
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
--- /dev/null
+ 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox)
+ 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++ file with gcc 4.4 (inbox, unread)
+ 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+ 2009-11-17 Keith Packard ╰┬► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+End of search results.
--- /dev/null
+ 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox, unread)
+ 2010-12-16 Olivier Berger ─►Essai accentué (inbox, unread)
+ 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox, unread)
+ 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment, inbox, unread)
+ 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox, unread)
+ 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox, signed, unread)
+ 2009-11-18 Keith Packard ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox, unread)
+ 2009-11-18 Keith Packard ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox, unread)
+ 2009-11-17 Jan Janak ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox, unread)
+ 2009-11-18 Keith Packard ╰┬► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
+ 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
+ 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox, signed, unread)
+ 2009-11-17 Mikhail Gusarov ├┬► ... (inbox, signed, unread)
+ 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox, signed, unread)
+ 2009-11-17 Mikhail Gusarov │ ├─► ... (inbox, unread)
+ 2009-11-17 Keith Packard │ ╰┬► ... (inbox, unread)
+ 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox, signed, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+ 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++ file with gcc 4.4 (inbox, unread)
+ 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+ 2009-11-17 Keith Packard ╰┬► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox, unread)
+ 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox, unread)
+ 2009-11-18 Jjgod Jiang ╰┬► ... (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰─► ... (inbox, unread)
+ 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox, unread)
+ 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox, unread)
+ 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment, inbox, signed, unread)
+ 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment, inbox, signed, unread)
+ 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox, unread)
+ 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment, inbox, unread)
+ 2009-11-17 Carl Worth ╰─► ... (inbox, unread)
+End of search results.
--- /dev/null
+ 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox, unread)
+ 2010-12-16 Olivier Berger ─►Essai accentué (inbox, test_tag, unread)
+ 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox, unread)
+ 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment, inbox, unread)
+ 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox, unread)
+ 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox, signed, unread)
+ 2009-11-18 Keith Packard ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox, unread)
+ 2009-11-18 Keith Packard ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox, unread)
+ 2009-11-17 Jan Janak ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox, unread)
+ 2009-11-18 Keith Packard ╰┬► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
+ 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
+ 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox, signed, unread)
+ 2009-11-17 Mikhail Gusarov ├┬► ... (inbox, signed, unread)
+ 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox, signed, unread)
+ 2009-11-17 Mikhail Gusarov │ ├─► ... (inbox, unread)
+ 2009-11-17 Keith Packard │ ╰┬► ... (inbox, unread)
+ 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox, signed, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+ 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++ file with gcc 4.4 (inbox, unread)
+ 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+ 2009-11-17 Keith Packard ╰┬► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox, unread)
+ 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox, unread)
+ 2009-11-18 Jjgod Jiang ╰┬► ... (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰─► ... (inbox, unread)
+ 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox, unread)
+ 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox, unread)
+ 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment, inbox, signed, unread)
+ 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment, inbox, signed, unread)
+ 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox, unread)
+ 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment, inbox, unread)
+ 2009-11-17 Carl Worth ╰─► ... (inbox, unread)
+End of search results.
--- /dev/null
+ 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox, unread)
+ 2010-12-16 Olivier Berger ─►Essai accentué (inbox, unread)
+ 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox, unread)
+ 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment, inbox, unread)
+ 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox, unread)
+ 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox, signed, unread)
+ 2009-11-18 Keith Packard ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox, unread)
+ 2009-11-18 Keith Packard ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox, unread)
+ 2009-11-17 Jan Janak ├─► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox, unread)
+ 2009-11-18 Keith Packard ╰┬► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
+ 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox, unread)
+ 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox, signed, test_thread_tag, unread)
+ 2009-11-17 Mikhail Gusarov ├┬► ... (inbox, signed, test_thread_tag, unread)
+ 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox, signed, test_thread_tag, unread)
+ 2009-11-17 Mikhail Gusarov │ ├─► ... (inbox, test_thread_tag, unread)
+ 2009-11-17 Keith Packard │ ╰┬► ... (inbox, test_thread_tag, unread)
+ 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox, signed, test_thread_tag, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, test_thread_tag, unread)
+ 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+ 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++ file with gcc 4.4 (inbox, unread)
+ 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox, unread)
+ 2009-11-17 Keith Packard ╰┬► ... (inbox, unread)
+ 2009-11-18 Carl Worth ╰─► ... (inbox, unread)
+ 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox, unread)
+ 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox, unread)
+ 2009-11-18 Jjgod Jiang ╰┬► ... (inbox, unread)
+ 2009-11-18 Alexander Botero-Low ╰─► ... (inbox, unread)
+ 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox, unread)
+ 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox, unread)
+ 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox, unread)
+ 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment, inbox, signed, unread)
+ 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment, inbox, signed, unread)
+ 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox, unread)
+ 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment, inbox, unread)
+ 2009-11-17 Carl Worth ╰─► ... (inbox, unread)
+End of search results.
--- /dev/null
+/* this file mimics the macros present in recent GCC and CLANG */
+
+#ifndef _ENDIAN_UTIL_H
+#define _ENDIAN_UTIL_H
+
+/* This are prefixed with UTIL to avoid collisions
+ *
+ * You can use something like the following to define UTIL_BYTE_ORDER
+ * in a configure script.
+ */
+#if 0
+#include <stdio.h>
+#include <stdint.h>
+uint32_t test = 0x34333231;
+int main() { printf("%.4s\n", (const char*)&test); return 0; }
+#endif
+
+#define UTIL_ORDER_BIG_ENDIAN 4321
+#define UTIL_ORDER_LITTLE_ENDIAN 1234
+
+
+#if !defined(UTIL_BYTE_ORDER) || ((UTIL_BYTE_ORDER != UTIL_ORDER_BIG_ENDIAN) && \
+ (UTIL_BYTE_ORDER != UTIL_ORDER_LITTLE_ENDIAN))
+#undef UTIL_BYTE_ORDER
+#ifdef __BYTE_ORDER__
+# if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+# define UTIL_BYTE_ORDER UTIL_ORDER_LITTLE_ENDIAN
+# elif (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
+# define UTIL_BYTE_ORDER UTIL_ORDER_BIG_ENDIAN
+# else
+# error "Unsupported __BYTE_ORDER__"
+# endif
+#else
+# error "UTIL_BYTE_ORDER not correctly defined and __BYTE_ORDER__ not defined."
+#endif
+#endif
+
+#endif
install:
$(INSTALL) $(CURDIR)/notmuch.vim $(D)$(prefix)/plugin/notmuch.vim
+ $(INSTALL) $(CURDIR)/notmuch.txt $(D)$(prefix)/doc/notmuch.txt
@$(foreach file,$(wildcard syntax/*), \
$(INSTALL) $(CURDIR)/$(file) $(D)$(prefix)/$(file);)
--- /dev/null
+*notmuch.txt* Plug-in to make vim a nice email client using notmuch
+
+Author: Felipe Contreras <felipe.contreras@gmail.com>
+
+Overview |notmuch-intro|
+Usage |notmuch-usage|
+Mappings |notmuch-mappings|
+Configuration |notmuch-config|
+
+==============================================================================
+OVERVIEW *notmuch-intro*
+
+This is a vim plug-in that provides a fully usable mail client interface,
+utilizing the notmuch framework.
+
+It has three main views: folders, search, and thread. In the folder view you
+can find a summary of saved searches, In the search view you can see all the
+threads that comprise the selected search, and in the thread view you can read
+every mail in the thread.
+
+==============================================================================
+USAGE *notmuch-usage*
+
+To use it, simply run the `:NotMuch` command.
+
+By default you start in the folder view which shows you default searches and
+the number of threads that match those:
+>
+ 10 new (tag:inbox and tag:unread)
+ 20 inbox (tag:inbox)
+ 30 unread (tag:unread)
+<
+You can see the threads of each by clicking `enter`, which sends you to the
+search view. In both the search and folder views you can type `s` to type a
+new search, or `=` to refresh. To see a thread, type `enter` again.
+
+To exit a view, click `q`.
+
+Also, you can specify a search directly:
+>
+ :NotMuch is:inbox and date:yesterday..
+<
+==============================================================================
+MAPPINGS *notmuch-mappings*
+
+------------------------------------------------------------------------------
+Folder view~
+
+<enter> Show selected search
+s Enter a new search
+= Refresh
+c Compose a new mail
+
+------------------------------------------------------------------------------
+Search view~
+
+q Quit view
+<enter> Show selected search
+<space> Show selected search with filter
+A Archive (-inbox -unread)
+I Mark as read (-unread)
+t Tag (prompted)
+s Search
+= Refresh
+? Show search information
+c Compose a new mail
+>
+------------------------------------------------------------------------------
+Thread view~
+
+q Quit view
+A Archive (-inbox -unread)
+I Mark as read (-unread)
+t Tag (prompted)
+s Search
+p Save patches
+r Reply
+? Show thread information
+<tab> Show next message
+c Compose a new mail
+
+------------------------------------------------------------------------------
+Compose view~
+
+q Quit view
+s Send
+
+==============================================================================
+CONFIGURATION *notmuch-config*
+
+You can add the following configurations to your `.vimrc`, or
+`~/.vim/plugin/notmuch.vim`.
+
+ *g:notmuch_folders*
+
+The first thing you might want to do is set your custom searches.
+>
+ let g:notmuch_folders = [
+ \ [ 'new', 'tag:inbox and tag:unread' ],
+ \ [ 'inbox', 'tag:inbox' ],
+ \ [ 'unread', 'tag:unread' ],
+ \ [ 'to-do', 'tag:to-do' ],
+ \ [ 'to-me', 'to:john.doe and tag:new' ],
+ \ ]
+<
+
+ *g:notmuch_custom_search_maps*
+ *g:notmuch_custom_show_maps*
+
+You can also configure the keyboard mappings for the different views:
+>
+ let g:notmuch_custom_search_maps = {
+ \ 't': 'search_tag("+to-do -inbox")',
+ \ 'd': 'search_tag("+deleted -inbox -unread")',
+ \ }
+
+ let g:notmuch_custom_show_maps = {
+ \ 't': 'show_tag("+to-do -inbox")',
+ \ 'd': 'show_tag("+deleted -inbox -unread")',
+ \ }
+<
+
+ *g:notmuch_date_format*
+
+To configure the date format you want in the search view:
+>
+ let g:notmuch_date_format = '%d.%m.%y'
+<
+
+ *g:notmuch_datetime_format*
+
+You can do the same for the thread view:
+>
+ let g:notmuch_datetime_format = '%d.%m.%y %H:%M:%S'
+<
+
+ *g:notmuch_folders_count_threads*
+
+If you want to count the threads instead of the messages in the folder view:
+>
+ let g:notmuch_folders_count_threads = 0
+<
+
+ *g:notmuch_reader*
+ *g:notmuch_sendmail*
+
+You can also configure your externail mail reader and sendemail program:
+>
+ let g:notmuch_reader = 'mutt -f %s'
+ let g:notmuch_sendmail = 'sendmail'
+<
+
+vim:tw=78:ts=8:noet:ft=help:
-if exists("g:loaded_notmuch_rb")
+if exists("g:loaded_notmuch")
finish
endif
finish
endif
-let g:loaded_notmuch_rb = "yep"
+let g:loaded_notmuch = "yep"
-let g:notmuch_rb_folders_maps = {
+let g:notmuch_folders_maps = {
\ '<Enter>': 'folders_show_search()',
\ 's': 'folders_search_prompt()',
\ '=': 'folders_refresh()',
+ \ 'c': 'compose()',
\ }
-let g:notmuch_rb_search_maps = {
+let g:notmuch_search_maps = {
\ 'q': 'kill_this_buffer()',
\ '<Enter>': 'search_show_thread(1)',
\ '<Space>': 'search_show_thread(2)',
\ 's': 'search_search_prompt()',
\ '=': 'search_refresh()',
\ '?': 'search_info()',
+ \ 'c': 'compose()',
\ }
-let g:notmuch_rb_show_maps = {
+let g:notmuch_show_maps = {
\ 'q': 'kill_this_buffer()',
\ 'A': 'show_tag("-inbox -unread")',
\ 'I': 'show_tag("-unread")',
\ 'o': 'show_open_msg()',
\ 'e': 'show_extract_msg()',
\ 's': 'show_save_msg()',
+ \ 'p': 'show_save_patches()',
\ 'r': 'show_reply()',
\ '?': 'show_info()',
\ '<Tab>': 'show_next_msg()',
+ \ 'c': 'compose()',
\ }
-let g:notmuch_rb_compose_maps = {
+let g:notmuch_compose_maps = {
\ ',s': 'compose_send()',
\ ',q': 'compose_quit()',
\ }
-let s:notmuch_rb_folders_default = [
+let s:notmuch_folders_default = [
\ [ 'new', 'tag:inbox and tag:unread' ],
\ [ 'inbox', 'tag:inbox' ],
\ [ 'unread', 'tag:unread' ],
\ ]
-let s:notmuch_rb_date_format_default = '%d.%m.%y'
-let s:notmuch_rb_datetime_format_default = '%d.%m.%y %H:%M:%S'
-let s:notmuch_rb_reader_default = 'xfce4-terminal -e "mutt -f %s"'
-let s:notmuch_rb_sendmail_default = 'sendmail'
-let s:notmuch_rb_folders_count_threads_default = 0
-
-if !exists('g:notmuch_rb_date_format')
- let g:notmuch_rb_date_format = s:notmuch_rb_date_format_default
-endif
-
-if !exists('g:notmuch_rb_datetime_format')
- let g:notmuch_rb_datetime_format = s:notmuch_rb_datetime_format_default
-endif
-
-if !exists('g:notmuch_rb_reader')
- let g:notmuch_rb_reader = s:notmuch_rb_reader_default
-endif
-
-if !exists('g:notmuch_rb_sendmail')
- let g:notmuch_rb_sendmail = s:notmuch_rb_sendmail_default
-endif
-
-if !exists('g:notmuch_rb_folders_count_threads')
- let g:notmuch_rb_folders_count_threads = s:notmuch_rb_folders_count_threads_default
-endif
+let s:notmuch_date_format_default = '%d.%m.%y'
+let s:notmuch_datetime_format_default = '%d.%m.%y %H:%M:%S'
+let s:notmuch_reader_default = 'mutt -f %s'
+let s:notmuch_sendmail_default = 'sendmail'
+let s:notmuch_folders_count_threads_default = 0
function! s:new_file_buffer(type, fname)
exec printf('edit %s', a:fname)
execute printf('set filetype=notmuch-%s', a:type)
execute printf('set syntax=notmuch-%s', a:type)
- ruby $buf_queue.push($curbuf.number)
+ ruby $curbuf.init(VIM::evaluate('a:type'))
endfunction
-function! s:compose_unload()
+function! s:on_compose_delete()
if b:compose_done
return
endif
function! s:show_reply()
ruby open_reply get_message.mail
let b:compose_done = 0
- call s:set_map(g:notmuch_rb_compose_maps)
- autocmd BufUnload <buffer> call s:compose_unload()
+ call s:set_map(g:notmuch_compose_maps)
+ autocmd BufDelete <buffer> call s:on_compose_delete()
+ startinsert!
+endfunction
+
+function! s:compose()
+ ruby open_compose
+ let b:compose_done = 0
+ call s:set_map(g:notmuch_compose_maps)
+ autocmd BufDelete <buffer> call s:on_compose_delete()
startinsert!
endfunction
ruby << EOF
m = get_message
mbox = File.expand_path('~/.notmuch/vim_mbox')
- cmd = VIM::evaluate('g:notmuch_rb_reader') % mbox
+ cmd = VIM::evaluate('g:notmuch_reader') % mbox
system "notmuch show --format=mbox id:#{m.message_id} > #{mbox} && #{cmd}"
EOF
endfunction
EOF
endfunction
+function! s:show_save_patches()
+ruby << EOF
+ q = $curbuf.query($cur_thread)
+ t = q.search_threads.first
+ n = 0
+ t.toplevel_messages.first.replies.each do |m|
+ next if not m['subject'] =~ /^\[PATCH.*\]/
+ file = "%04d.patch" % [n += 1]
+ system "notmuch show --format=mbox id:#{m.message_id} > #{file}"
+ end
+ vim_puts "Saved #{n} patches"
+EOF
+endfunction
+
function! s:show_tag(intags)
if empty(a:intags)
let tags = input('tags: ')
function! s:search_search_prompt()
let text = input('Search: ')
+ if text == ""
+ return
+ endif
setlocal modifiable
ruby << EOF
$cur_search = VIM::evaluate('text')
+ $curbuf.reopen
search_render($cur_search)
EOF
setlocal nomodifiable
function! s:search_refresh()
setlocal modifiable
+ ruby $curbuf.reopen
ruby search_render($cur_search)
setlocal nomodifiable
endfunction
endif
ruby do_tag(get_thread_id, VIM::evaluate('l:tags'))
norm j
- call s:search_refresh()
endfunction
function! s:folders_search_prompt()
function! s:folders_refresh()
setlocal modifiable
+ ruby $curbuf.reopen
ruby folders_render()
setlocal nomodifiable
endfunction
endfunction
function! s:kill_this_buffer()
- bdelete!
ruby << EOF
- $buf_queue.pop
- b = $buf_queue.last
- VIM::command("buffer #{b}") if b
+ $curbuf.close
+ VIM::command("bdelete!")
EOF
endfunction
keepjumps 0d
execute printf('set filetype=notmuch-%s', a:type)
execute printf('set syntax=notmuch-%s', a:type)
- ruby $buf_queue.push($curbuf.number)
+ ruby $curbuf.init(VIM::evaluate('a:type'))
endfunction
function! s:set_menu_buffer()
$cur_thread = thread_id
$messages.clear
$curbuf.render do |b|
- do_read do |db|
- q = db.query(get_cur_view)
- q.sort = 0
- msgs = q.search_messages
- msgs.each do |msg|
- m = Mail.read(msg.filename)
- part = m.find_first_text
- nm_m = Message.new(msg, m)
- $messages << nm_m
- date_fmt = VIM::evaluate('g:notmuch_rb_datetime_format')
- date = Time.at(msg.date).strftime(date_fmt)
- nm_m.start = b.count
- b << "%s %s (%s)" % [msg['from'], date, msg.tags]
- b << "Subject: %s" % [msg['subject']]
- b << "To: %s" % m['to']
- b << "Cc: %s" % m['cc']
- b << "Date: %s" % m['date']
- nm_m.body_start = b.count
- b << "--- %s ---" % part.mime_type
- part.convert.each_line do |l|
- b << l.chomp
- end
- b << ""
- nm_m.end = b.count
+ q = $curbuf.query(get_cur_view)
+ q.sort = Notmuch::SORT_OLDEST_FIRST
+ msgs = q.search_messages
+ msgs.each do |msg|
+ m = Mail.read(msg.filename)
+ part = m.find_first_text
+ nm_m = Message.new(msg, m)
+ $messages << nm_m
+ date_fmt = VIM::evaluate('g:notmuch_datetime_format')
+ date = Time.at(msg.date).strftime(date_fmt)
+ nm_m.start = b.count
+ b << "%s %s (%s)" % [msg['from'], date, msg.tags]
+ b << "Subject: %s" % [msg['subject']]
+ b << "To: %s" % msg['to']
+ b << "Cc: %s" % msg['cc']
+ b << "Date: %s" % msg['date']
+ nm_m.body_start = b.count
+ b << "--- %s ---" % part.mime_type
+ part.convert.each_line do |l|
+ b << l.chomp
end
- b.delete(b.count)
+ b << ""
+ nm_m.end = b.count
end
+ b.delete(b.count)
end
$messages.each_with_index do |msg, i|
VIM::command("syntax region nmShowMsg#{i}Desc start='\\%%%il' end='\\%%%il' contains=@nmShowMsgDesc" % [msg.start, msg.start + 1])
end
EOF
setlocal nomodifiable
- call s:set_map(g:notmuch_rb_show_maps)
+ call s:set_map(g:notmuch_show_maps)
endfunction
function! s:search_show_thread(mode)
search_render($cur_search)
EOF
call s:set_menu_buffer()
- call s:set_map(g:notmuch_rb_search_maps)
+ call s:set_map(g:notmuch_search_maps)
autocmd CursorMoved <buffer> call s:show_cursor_moved()
endfunction
call s:new_buffer('folders')
ruby folders_render()
call s:set_menu_buffer()
- call s:set_map(g:notmuch_rb_folders_maps)
+ call s:set_map(g:notmuch_folders_maps)
endfunction
"" root
function! s:set_defaults()
- if exists('g:notmuch_rb_custom_search_maps')
- call extend(g:notmuch_rb_search_maps, g:notmuch_rb_custom_search_maps)
+ if !exists('g:notmuch_date_format')
+ if exists('g:notmuch_rb_date_format')
+ let g:notmuch_date_format = g:notmuch_rb_date_format
+ else
+ let g:notmuch_date_format = s:notmuch_date_format_default
+ endif
endif
- if exists('g:notmuch_rb_custom_show_maps')
- call extend(g:notmuch_rb_show_maps, g:notmuch_rb_custom_show_maps)
+ if !exists('g:notmuch_datetime_format')
+ if exists('g:notmuch_rb_datetime_format')
+ let g:notmuch_datetime_format = g:notmuch_rb_datetime_format
+ else
+ let g:notmuch_datetime_format = s:notmuch_datetime_format_default
+ endif
endif
- " TODO for now lets check the old folders too
- if !exists('g:notmuch_rb_folders')
- if exists('g:notmuch_folders')
- let g:notmuch_rb_folders = g:notmuch_folders
+ if !exists('g:notmuch_reader')
+ if exists('g:notmuch_rb_reader')
+ let g:notmuch_reader = g:notmuch_rb_reader
else
- let g:notmuch_rb_folders = s:notmuch_rb_folders_default
+ let g:notmuch_reader = s:notmuch_reader_default
+ endif
+ endif
+
+ if !exists('g:notmuch_sendmail')
+ if exists('g:notmuch_rb_sendmail')
+ let g:notmuch_sendmail = g:notmuch_rb_sendmail
+ else
+ let g:notmuch_sendmail = s:notmuch_sendmail_default
+ endif
+ endif
+
+ if !exists('g:notmuch_folders_count_threads')
+ if exists('g:notmuch_rb_count_threads')
+ let g:notmuch_count_threads = g:notmuch_rb_count_threads
+ else
+ let g:notmuch_folders_count_threads = s:notmuch_folders_count_threads_default
+ endif
+ endif
+
+ if !exists('g:notmuch_custom_search_maps') && exists('g:notmuch_rb_custom_search_maps')
+ let g:notmuch_custom_search_maps = g:notmuch_rb_custom_search_maps
+ endif
+
+ if !exists('g:notmuch_custom_show_maps') && exists('g:notmuch_rb_custom_show_maps')
+ let g:notmuch_custom_show_maps = g:notmuch_rb_custom_show_maps
+ endif
+
+ if exists('g:notmuch_custom_search_maps')
+ call extend(g:notmuch_search_maps, g:notmuch_custom_search_maps)
+ endif
+
+ if exists('g:notmuch_custom_show_maps')
+ call extend(g:notmuch_show_maps, g:notmuch_custom_show_maps)
+ endif
+
+ if !exists('g:notmuch_folders')
+ if exists('g:notmuch_rb_folders')
+ let g:notmuch_folders = g:notmuch_rb_folders
+ else
+ let g:notmuch_folders = s:notmuch_folders_default
endif
endif
endfunction
-function! s:NotMuch()
+function! s:NotMuch(...)
call s:set_defaults()
ruby << EOF
require 'notmuch'
require 'rubygems'
require 'tempfile'
+ require 'socket'
begin
require 'mail'
rescue LoadError
end
$db_name = nil
- $email_address = nil
+ $email = $email_name = $email_address = nil
$searches = []
- $buf_queue = []
$threads = []
$messages = []
$config = {}
end
$db_name = $config['database.path']
- $email_address = "%s <%s>" % [$config['user.name'], $config['user.primary_email']]
+ $email_name = $config['user.name']
+ $email_address = $config['user.primary_email']
+ $email = "%s <%s>" % [$email_name, $email_address]
end
def vim_puts(s)
end
end
- def do_write
- db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE)
- begin
- yield db
- ensure
- db.close
- end
+ def generate_message_id
+ t = Time.now
+ random_tag = sprintf('%x%x_%x%x%x',
+ t.to_i, t.tv_usec,
+ $$, Thread.current.object_id.abs, rand(255))
+ return "<#{random_tag}@#{Socket.gethostname}.notmuch>"
end
- def do_read
- db = Notmuch::Database.new($db_name)
- begin
- yield db
- ensure
- db.close
- end
- end
-
- def open_reply(orig)
+ def open_compose_helper(lines, cur)
help_lines = [
'Notmuch-Help: Type in your message here; to help you use these bindings:',
'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)',
'Notmuch-Help: ,q - abort the message',
]
+
+ dir = File.expand_path('~/.notmuch/compose')
+ FileUtils.mkdir_p(dir)
+ Tempfile.open(['nm-', '.mail'], dir) do |f|
+ f.puts(help_lines)
+ f.puts
+ f.puts(lines)
+
+ sig_file = File.expand_path('~/.signature')
+ if File.exists?(sig_file)
+ f.puts("-- ")
+ f.write(File.read(sig_file))
+ end
+
+ f.flush
+
+ cur += help_lines.size + 1
+
+ VIM::command("let s:reply_from='%s'" % $email_address)
+ VIM::command("call s:new_file_buffer('compose', '#{f.path}')")
+ VIM::command("call cursor(#{cur}, 0)")
+ end
+ end
+
+ def open_reply(orig)
reply = orig.reply do |m|
# fix headers
if not m[:reply_to]
m.to = [orig[:from].to_s, orig[:to].to_s]
end
m.cc = orig[:cc]
- m.from = $email_address
+ m.from = $email
+ m.message_id = generate_message_id
m.charset = 'utf-8'
m.content_transfer_encoding = '7bit'
end
- dir = File.expand_path('~/.notmuch/compose')
- FileUtils.mkdir_p(dir)
- Tempfile.open(['nm-', '.mail'], dir) do |f|
- lines = []
-
- lines += help_lines
- lines << ''
-
- body_lines = []
- if $mail_installed
- addr = Mail::Address.new(orig[:from].value)
- name = addr.name
- name = addr.local + "@" if name.nil? && !addr.local.nil?
- else
- name = orig[:from]
- end
- name = "somebody" if name.nil?
+ lines = []
- body_lines << "%s wrote:" % name
- part = orig.find_first_text
- part.convert.each_line do |l|
- body_lines << "> %s" % l.chomp
- end
- body_lines << ""
- body_lines << ""
- body_lines << ""
-
- reply.body = body_lines.join("\n")
+ body_lines = []
+ if $mail_installed
+ addr = Mail::Address.new(orig[:from].value)
+ name = addr.name
+ name = addr.local + "@" if name.nil? && !addr.local.nil?
+ else
+ name = orig[:from]
+ end
+ name = "somebody" if name.nil?
- lines += reply.to_s.lines.map { |e| e.chomp }
- lines << ""
+ body_lines << "%s wrote:" % name
+ part = orig.find_first_text
+ part.convert.each_line do |l|
+ body_lines << "> %s" % l.chomp
+ end
+ body_lines << ""
+ body_lines << ""
+ body_lines << ""
- old_count = lines.count - 1
+ reply.body = body_lines.join("\n")
- f.puts(lines)
+ lines += reply.to_s.lines.map { |e| e.chomp }
+ lines << ""
- sig_file = File.expand_path('~/.signature')
- if File.exists?(sig_file)
- f.puts("-- ")
- f.write(File.read(sig_file))
- end
+ cur = lines.count - 1
- f.flush
+ open_compose_helper(lines, cur)
+ end
- VIM::command("let s:reply_from='%s'" % reply.from.first.to_s)
- VIM::command("call s:new_file_buffer('compose', '#{f.path}')")
- VIM::command("call cursor(#{old_count}, 0)")
- end
+ def open_compose()
+ lines = []
+
+ lines << "Date: #{Time.now().strftime('%a, %-d %b %Y %T %z')}"
+ lines << "From: #{$email}"
+ lines << "To: "
+ cur = lines.count
+
+ lines << "Cc: "
+ lines << "Bcc: "
+ lines << "Message-Id: #{generate_message_id}"
+ lines << "Subject: "
+ lines << "Mime-Version: 1.0"
+ lines << "Content-Type: text/plain; charset=utf-8"
+ lines << "Content-Transfer-Encoding: 7bit"
+ lines << ""
+ lines << ""
+ lines << ""
+
+ open_compose_helper(lines, cur)
end
def folders_render()
$curbuf.render do |b|
- folders = VIM::evaluate('g:notmuch_rb_folders')
- count_threads = VIM::evaluate('g:notmuch_rb_folders_count_threads')
+ folders = VIM::evaluate('g:notmuch_folders')
+ count_threads = VIM::evaluate('g:notmuch_folders_count_threads')
$searches.clear
- do_read do |db|
- folders.each do |name, search|
- q = db.query(search)
- $searches << search
- count = count_threads ? q.search_threads.count : q.search_messages.count
- b << "%9d %-20s (%s)" % [count, name, search]
- end
+ folders.each do |name, search|
+ q = $curbuf.query(search)
+ $searches << search
+ count = count_threads ? q.search_threads.count : q.search_messages.count
+ b << "%9d %-20s (%s)" % [count, name, search]
end
end
end
def search_render(search)
- date_fmt = VIM::evaluate('g:notmuch_rb_date_format')
- db = Notmuch::Database.new($db_name)
- q = db.query(search)
+ date_fmt = VIM::evaluate('g:notmuch_date_format')
+ q = $curbuf.query(search)
+ q.sort = Notmuch::SORT_NEWEST_FIRST
$threads.clear
t = q.search_threads
items.each do |e|
authors = e.authors.to_utf8.split(/[,|]/).map { |a| author_filter(a) }.join(",")
date = Time.at(e.newest_date).strftime(date_fmt)
+ subject = e.messages.first['subject']
if $mail_installed
- subject = Mail::Field.new("Subject: " + e.subject).to_s
+ subject = Mail::Field.new("Subject: " + subject).to_s
else
- subject = e.subject.force_encoding('utf-8')
+ subject = subject.force_encoding('utf-8')
end
b << "%-12s %3s %-20.20s | %s (%s)" % [date, e.matched_messages, authors, subject, e.tags]
$threads << e.thread_id
end
def do_tag(filter, tags)
- do_write do |db|
+ $curbuf.do_write do |db|
q = db.query(filter)
q.search_messages.each do |e|
e.freeze
e.thaw
e.tags_to_maildir_flags
end
+ q.destroy!
+ end
+ end
+
+ module DbHelper
+ def init(name)
+ @name = name
+ @db = Notmuch::Database.new($db_name)
+ @queries = []
+ end
+
+ def query(*args)
+ q = @db.query(*args)
+ @queries << q
+ q
+ end
+
+ def close
+ @queries.delete_if { |q| ! q.destroy! }
+ @db.close
+ end
+
+ def reopen
+ close if @db
+ @db = Notmuch::Database.new($db_name)
+ end
+
+ def do_write
+ db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE)
+ begin
+ yield db
+ ensure
+ db.close
+ end
end
end
end
class VIM::Buffer
+ include DbHelper
+
def <<(a)
append(count(), a)
end
get_config
EOF
- call s:folders()
+ if a:0
+ call s:search(join(a:000))
+ else
+ call s:folders()
+ endif
endfunction
-command NotMuch :call s:NotMuch()
+command -nargs=* NotMuch call s:NotMuch(<f-args>)
" vim: set noexpandtab:
description: "notmuch mail user interface"
files:
- plugin/notmuch.vim
+ - doc/notmuch.txt
- syntax/notmuch-compose.vim
- syntax/notmuch-folders.vim
- syntax/notmuch-git-diff.vim