]> git.notmuchmail.org Git - notmuch/commitdiff
Merge tag 'debian/0.37-1' into debian/bullseye-backports
authorDavid Bremner <david@tethera.net>
Mon, 10 Oct 2022 15:56:18 +0000 (12:56 -0300)
committerDavid Bremner <david@tethera.net>
Mon, 10 Oct 2022 15:56:18 +0000 (12:56 -0300)
notmuch release 0.37-1 for unstable (sid) [dgit]

[dgit distro=debian no-split --quilt=linear]

102 files changed:
.gitignore
Makefile.local
NEWS
bindings/python-cffi/notmuch2/_build.py
bindings/python-cffi/notmuch2/_message.py
bindings/python-cffi/tests/conftest.py
bindings/python/notmuch/version.py
configure
debian/changelog
debian/control
debian/notmuch-git.install [new file with mode: 0644]
debian/notmuch-git.manpages [new file with mode: 0644]
devel/emacs-keybindings.org
devel/nmbug/nmbug [deleted file]
devel/schemata
doc/Makefile.local
doc/command-line.rst [new file with mode: 0644]
doc/conf.py
doc/elisp.py [new file with mode: 0644]
doc/index.rst
doc/man1/notmuch-config.rst
doc/man1/notmuch-git.rst [new file with mode: 0644]
doc/man1/notmuch-insert.rst
doc/man1/notmuch-new.rst
doc/man1/notmuch-reply.rst
doc/man1/notmuch-show.rst
doc/man1/notmuch.rst
doc/man7/notmuch-sexp-queries.rst
doc/notmuch-emacs.rst
doc/queries.rst [new file with mode: 0644]
emacs/notmuch-lib.el
emacs/notmuch-mua.el
emacs/notmuch-query.el
emacs/notmuch-show.el
emacs/notmuch-tree.el
emacs/notmuch.el
hooks.c
lib/database.cc
lib/message.cc
lib/notmuch-private.h
lib/notmuch.h
lib/parse-sexp.cc
lib/tags.c
mime-node.c
notmuch-client.h
notmuch-git.py [new file with mode: 0644]
notmuch-reply.c
notmuch-show.c
notmuch.c
performance-test/T06-emacs.sh
performance-test/T07-git.sh [new file with mode: 0755]
test/T055-path-config.sh
test/T070-insert.sh
test/T081-sexpr-search.sh
test/T160-json.sh
test/T170-sexp.sh
test/T190-multipart.sh
test/T210-raw.sh
test/T220-reply.sh
test/T400-hooks.sh
test/T405-external.sh [new file with mode: 0755]
test/T450-emacs-show.sh
test/T453-emacs-reply.sh [new file with mode: 0755]
test/T454-emacs-dont-reply-names.sh [new file with mode: 0755]
test/T460-emacs-tree.sh
test/T465-emacs-unthreaded.sh
test/T520-show.sh
test/T560-lib-error.sh
test/T562-lib-database.sh
test/T563-lib-directory.sh
test/T564-lib-query.sh
test/T565-lib-tags.sh [new file with mode: 0755]
test/T566-lib-message.sh
test/T568-lib-thread.sh
test/T570-revision-tracking.sh
test/T610-message-property.sh
test/T620-lock.sh
test/T640-database-modified.sh
test/T720-lib-lifetime.sh
test/T850-git.sh [new file with mode: 0755]
test/corpora/attachment/x-gtar-compressed.eml [new file with mode: 0644]
test/corpora/default/bar/baz/05:2,
test/corpora/duplicate/msg-1-1:2, [new file with mode: 0644]
test/corpora/duplicate/msg-1-2:2, [new file with mode: 0644]
test/corpora/duplicate/msg-2-1:2, [new file with mode: 0644]
test/corpora/duplicate/msg-2-2:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-1:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-2:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-3:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-4:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-5:2, [new file with mode: 0644]
test/emacs-reply.expected-output/notmuch-reply-duplicate-4 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-depth [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-depth-1 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-duplicate-4 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-height-0 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-multipart-alternative [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-size [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-size-450 [new file with mode: 0644]
test/emacs.expected-output/raw-message-cf0c4d-52ad0a
test/test-lib.sh
version.txt

index f846ebecf1e5ece7c8b309c4f1ea9e5e2706d3d8..f94d1480c1f91d5ff64e8a0f746f4f79de19b839 100644 (file)
@@ -10,7 +10,9 @@
 /bindings/python-cffi/build/
 /lib/libnotmuch*.dylib
 /lib/libnotmuch.so*
+/nmbug
 /notmuch
+/notmuch-git
 /notmuch-shared
 /releases
 /sh.config
index d8bbf3e17b1f90ab7a54722c0d704307037478ef..7699c208bb8eca696a35ae7ea0951a6fd8c44487 100644 (file)
@@ -1,7 +1,7 @@
 # -*- makefile-gmake -*-
 
 .PHONY: all
-all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings
+all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings notmuch-git nmbug
 ifeq ($(MAKECMDGOALS),)
 ifeq ($(shell cat .first-build-message 2>/dev/null),)
        @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
@@ -45,6 +45,15 @@ $(SHA256_FILE): $(TAR_FILE)
 $(DETACHED_SIG_FILE): $(TAR_FILE)
        gpg --armor --detach-sign $^
 
+CLEAN := $(CLEAN) notmuch-git
+notmuch-git: notmuch-git.py
+       cp $< $@
+       chmod ugo+x $@
+
+CLEAN := $(CLEAN) nmbug
+nmbug: notmuch-git
+       ln -s $< $@
+
 .PHONY: dist
 dist: $(TAR_FILE)
 
diff --git a/NEWS b/NEWS
index c18d63d3212aaba4873e12c0d02780f63b5c4ec9..b83551f1f132c812e9fc888ca7fb90d57eee666a 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,51 @@
+Notmuch 0.37 (2022-08-21)
+=========================
+
+Library
+-------
+
+Fix uninitialized field in message objects.
+
+Improve exception handling and error propagation for message objects.
+
+Sexp Queries
+------------
+
+Add one sided lastmod ranges for sexp queries.
+
+Expand macro parameters inside regex and wildcard modifiers.
+
+Command Line Interface
+----------------------
+
+`notmuch help` now works for external commands.
+
+`NOTMUCH_CONFIG` is now passed to external commands and hooks.
+
+Promote the development tool `nmbug` to a user facing tool
+`notmuch-git`. See notmuch-git(1) for details.
+
+Emacs
+-----
+
+The function `notmuch-mua-mail` now moves point depending on the
+provided arguments.
+
+Restrict what mime types are inlined in replies and on refresh.
+
+The functions in notmuch-query.el are now obsolete and may be removed
+in a future version of Notmuch.
+
+Add some controls for lazy display of message bodies (See "Dealing
+with large messages and threads" in the notmuch-emacs documentation).
+
+Allow the user to select (with '%') a different duplicate message file
+to display.
+
+Use `message-dont-reply-to-names` in `notmuch-message-mode`.
+
+Support custom header-line format for notmuch-show mode.
+
 Notmuch 0.36 (2022-04-25)
 =========================
 
index 349bb79d5f746a3dc3973edf94ac60905c9186f3..65d7dcb6024cb576ffa0c887591b8d35a773d667 100644 (file)
@@ -55,6 +55,7 @@ ffibuilder.cdef(
         NOTMUCH_STATUS_DATABASE_EXISTS,
         NOTMUCH_STATUS_BAD_QUERY_SYNTAX,
         NOTMUCH_STATUS_NO_MAIL_ROOT,
+        NOTMUCH_STATUS_CLOSED_DATABASE,
         NOTMUCH_STATUS_LAST_STATUS
     } notmuch_status_t;
     typedef enum {
index aa1cb8757bfb3b0c1555a4bb8f7f9ae57b6c4b32..d4b34e91e2f88ff367a73cfa5b311d11cbfa6b9a 100644 (file)
@@ -26,7 +26,7 @@ class Message(base.NotmuchObject):
     package from Python's standard library.  You could e.g. create
     this as such::
 
-       notmuch_msg = db.get_message(msgid)  # or from a query
+       notmuch_msg = db.find(msgid)  # or from a query
        parser = email.parser.BytesParser(policy=email.policy.default)
        with notmuch_msg.path.open('rb) as fp:
            email_msg = parser.parse(fp)
index 6835fd30f723509cd6235540d35deadfc838cfa7..fe90c7879d8ad7ef0babb85386d7c87c2706a47c 100644 (file)
@@ -43,7 +43,7 @@ def notmuch(maildir):
         env = os.environ.copy()
         env['NOTMUCH_CONFIG'] = str(cfg_fname)
         proc = subprocess.run(cmd,
-                              timeout=5,
+                              timeout=120,
                               env=env)
         proc.check_returncode()
     return run
index 591446261914ff51e021bce490219a4e830d2bd8..943f335cb7b5b011ffc7c7d12b808985006dcc82 100644 (file)
@@ -1,3 +1,3 @@
 # this file should be kept in sync with ../../../version
-__VERSION__ = '0.36'
+__VERSION__ = '0.37'
 SOVERSION = '5'
index 2f6d8b687b46eea3d84f7ec4af8291a5f8b76ed3..5247e05a9754a9fa545cb576ea38e8711539d301 100755 (executable)
--- a/configure
+++ b/configure
@@ -564,7 +564,6 @@ int main () {
     GMimeSignature *sig = NULL;
     GMimeCertificate *cert = NULL;
     GMimeObject *output = NULL;
-    GMimeValidity validity = GMIME_VALIDITY_UNKNOWN;
     int len;
 
     g_mime_init ();
@@ -586,7 +585,7 @@ int main () {
     cert = g_mime_signature_get_certificate (sig);
     if (cert == NULL) return !! fprintf (stderr, "no GMimeCertificate found\n");
 #ifdef CHECK_VALIDITY
-    validity = g_mime_certificate_get_id_validity (cert);
+    GMimeValidity validity = g_mime_certificate_get_id_validity (cert);
     if (validity != GMIME_VALIDITY_FULL) return !! fprintf (stderr, "Got validity %d, expected %d\n", validity, GMIME_VALIDITY_FULL);
 #endif
 #ifdef CHECK_EMAIL
@@ -1567,9 +1566,17 @@ cat > sh.config <<EOF
 
 NOTMUCH_SRCDIR='${NOTMUCH_SRCDIR}'
 
+# Flags needed to compile and link against Xapian
+NOTMUCH_XAPIAN_CXXFLAGS="${xapian_cxxflags}"
+NOTMUCH_XAPIAN_LDFLAGS="${xapian_ldflags}"
+
 # Whether to have Xapian retry lock
 NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK=${WITH_RETRY_LOCK}
 
+# Flags needed to compile and link against GMime
+NOTMUCH_GMIME_CFLAGS="${gmime_cflags}"
+NOTMUCH_GMIME_LDFLAGS="${gmime_ldflags}"
+
 # Whether GMime can verify X.509 certificate validity
 NOTMUCH_GMIME_X509_CERT_VALIDITY=${gmime_x509_cert_validity}
 
@@ -1579,6 +1586,10 @@ NOTMUCH_GMIME_EMITS_ANGLE_BRACKETS=${gmime_emits_angle_brackets}
 # Whether GMime can verify signatures when decrypting with a session key:
 NOTMUCH_GMIME_VERIFY_WITH_SESSION_KEY=${gmime_verify_with_session_key}
 
+# Flags needed to compile and link against zlib
+NOTMUCH_ZLIB_CFLAGS="${zlib_cflags}"
+NOTMUCH_ZLIB_LDFLAGS="${zlib_ldflags}"
+
 # Does the C compiler support the address sanitizer
 NOTMUCH_HAVE_ASAN=${have_asan}
 
@@ -1615,6 +1626,10 @@ NOTMUCH_HAVE_PYTHON3_PYTEST=${have_python3_pytest}
 # Is the sfsexp library available?
 NOTMUCH_HAVE_SFSEXP=${have_sfsexp}
 
+# And if so, flags needed at compile/link time for sfsexp
+NOTMUCH_SFSEXP_CFLAGS="${sfsexp_cflags}"
+NOTMUCH_SFSEXP_LDFLAGS="${sfsexp_ldflags}"
+
 # Platform we are run on
 PLATFORM=${platform}
 EOF
index b45d11b5d9066268ff8fcf943236ca2184e250aa..2680c766429c6848fdd83a12a3ce659f9d8927d1 100644 (file)
@@ -1,3 +1,28 @@
+notmuch (0.37-1) unstable; urgency=medium
+
+  * New upstream release.
+  * Build-depend on emacs-el to work around #1017698
+
+ -- David Bremner <bremner@debian.org>  Wed, 24 Aug 2022 09:12:19 -0700
+
+notmuch (0.37~rc0-3) experimental; urgency=medium
+
+  * Another no-change re-upload with binaries.
+
+ -- David Bremner <bremner@debian.org>  Sun, 14 Aug 2022 11:49:21 -0300
+
+notmuch (0.37~rc0-2) experimental; urgency=medium
+
+  * Binary upload for NEW (notmuch-git is a new binary package)
+
+ -- David Bremner <bremner@debian.org>  Sun, 14 Aug 2022 10:55:24 -0300
+
+notmuch (0.37~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sun, 14 Aug 2022 07:28:22 -0300
+
 notmuch (0.36-1~bpo11+1) bullseye-backports; urgency=medium
 
   * Rebuild for bullseye-backports.
index a11d41306a17f11e9552370e974ab76e1a7b737b..2dcb8cc72f7caef524b07031323b98f3fc639e55 100644 (file)
@@ -19,7 +19,9 @@ Build-Depends:
  dpkg-dev (>= 1.17.14),
  dtach (>= 0.8) <!nocheck>,
  emacs-nox | emacs-gtk | emacs-lucid | emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~),
+ emacs-el,
  gdb [!ia64 !mips !mips64el !kfreebsd-any !alpha !hppa] <!nocheck>,
+ git <!nocheck>,
  gnupg <!nocheck>,
  gpgsm <!nocheck>,
  libgmime-3.0-dev (>= 3.0.3~),
@@ -66,6 +68,22 @@ Description: thread-based email index, search and tagging
  .
  This package contains the notmuch command-line interface
 
+Package: notmuch-git
+Architecture: all
+Depends:
+ git,
+ notmuch,
+ python3,
+ ${misc:Depends}
+Description: thread-based email index, search and tagging
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package contains a simple tool to save, restore, and synchronize
+ notmuch tags via git repositories.
+
 Package: notmuch-doc
 Architecture: all
 Depends:
diff --git a/debian/notmuch-git.install b/debian/notmuch-git.install
new file mode 100644 (file)
index 0000000..2be0827
--- /dev/null
@@ -0,0 +1,2 @@
+notmuch-git /usr/bin
+nmbug /usr/bin
diff --git a/debian/notmuch-git.manpages b/debian/notmuch-git.manpages
new file mode 100644 (file)
index 0000000..e0895c8
--- /dev/null
@@ -0,0 +1,2 @@
+usr/share/man/man1/notmuch-git.1.gz
+usr/share/man/man1/nmbug.1.gz
index 00977bc3a11213030320327965c77eac61b577df..ad7f72ef0bb3ce039a9bd3fd22988c93b80565c6 100644 (file)
@@ -40,6 +40,7 @@
 | Z            | notmuch-tree-from-search-current-query | notmuch-tree-from-show-current-query                  |                                         |
 | =!=          |                                        | notmuch-show-toggle-elide-non-matching                |                                         |
 | =#=          |                                        | notmuch-show-print-message                            |                                         |
+| =%=          |                                        | notmuch-show-replace-msg                              |                                         |
 | =$=          |                                        | notmuch-show-toggle-process-crypto                    |                                         |
 | =*=          | notmuch-search-tag-all                 | notmuch-show-tag-all                                  | notmuch-tree-tag-thread                 |
 | +            | notmuch-search-add-tag                 | notmuch-show-add-tag                                  | notmuch-tree-add-tag                    |
diff --git a/devel/nmbug/nmbug b/devel/nmbug/nmbug
deleted file mode 100755 (executable)
index 043c186..0000000
+++ /dev/null
@@ -1,852 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (c) 2011-2014 David Bremner <david@tethera.net>
-#                         W. Trevor King <wking@tremily.us>
-#
-# 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 https://www.gnu.org/licenses/ .
-
-"""
-Manage notmuch tags with Git
-
-Environment variables:
-
-* NMBGIT specifies the location of the git repository used by nmbug.
-  If not specified $HOME/.nmbug is used.
-* NMBPREFIX specifies the prefix in the notmuch database for tags of
-  interest to nmbug. If not specified 'notmuch::' is used.
-"""
-
-from __future__ import print_function
-from __future__ import unicode_literals
-
-import codecs as _codecs
-import collections as _collections
-import functools as _functools
-import inspect as _inspect
-import locale as _locale
-import logging as _logging
-import os as _os
-import re as _re
-import shutil as _shutil
-import subprocess as _subprocess
-import sys as _sys
-import tempfile as _tempfile
-import textwrap as _textwrap
-try:  # Python 3
-    from urllib.parse import quote as _quote
-    from urllib.parse import unquote as _unquote
-except ImportError:  # Python 2
-    from urllib import quote as _quote
-    from urllib import unquote as _unquote
-
-
-__version__ = '0.3'
-
-_LOG = _logging.getLogger('nmbug')
-_LOG.setLevel(_logging.WARNING)
-_LOG.addHandler(_logging.StreamHandler())
-
-NMBGIT = _os.path.expanduser(
-    _os.getenv('NMBGIT', _os.path.join('~', '.nmbug')))
-_NMBGIT = _os.path.join(NMBGIT, '.git')
-if _os.path.isdir(_NMBGIT):
-    NMBGIT = _NMBGIT
-
-TAG_PREFIX = _os.getenv('NMBPREFIX', 'notmuch::')
-_HEX_ESCAPE_REGEX = _re.compile('%[0-9A-F]{2}')
-_TAG_DIRECTORY = 'tags/'
-_TAG_FILE_REGEX = _re.compile(_TAG_DIRECTORY + '(?P<id>[^/]*)/(?P<tag>[^/]*)')
-
-# magic hash for Git (git hash-object -t blob /dev/null)
-_EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
-
-
-try:
-    getattr(_tempfile, 'TemporaryDirectory')
-except AttributeError:  # Python < 3.2
-    class _TemporaryDirectory(object):
-        """
-        Fallback context manager for Python < 3.2
-
-        See PEP 343 for details on context managers [1].
-
-        [1]: https://www.python.org/dev/peps/pep-0343/
-        """
-        def __init__(self, **kwargs):
-            self.name = _tempfile.mkdtemp(**kwargs)
-
-        def __enter__(self):
-            return self.name
-
-        def __exit__(self, type, value, traceback):
-            _shutil.rmtree(self.name)
-
-
-    _tempfile.TemporaryDirectory = _TemporaryDirectory
-
-
-def _hex_quote(string, safe='+@=:,'):
-    """
-    quote('abc def') -> 'abc%20def'.
-
-    Wrap urllib.parse.quote with additional safe characters (in
-    addition to letters, digits, and '_.-') and lowercase hex digits
-    (e.g. '%3a' instead of '%3A').
-    """
-    uppercase_escapes = _quote(string, safe)
-    return _HEX_ESCAPE_REGEX.sub(
-        lambda match: match.group(0).lower(),
-        uppercase_escapes)
-
-
-_ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,')  # quote ':'
-
-
-def _xapian_quote(string):
-    """
-    Quote a string for Xapian's QueryParser.
-
-    Xapian uses double-quotes for quoting strings.  You can escape
-    internal quotes by repeating them [1,2,3].
-
-    [1]: https://trac.xapian.org/ticket/128#comment:2
-    [2]: https://trac.xapian.org/ticket/128#comment:17
-    [3]: https://trac.xapian.org/changeset/13823/svn
-    """
-    return '"{0}"'.format(string.replace('"', '""'))
-
-
-def _xapian_unquote(string):
-    """
-    Unquote a Xapian-quoted string.
-    """
-    if string.startswith('"') and string.endswith('"'):
-        return string[1:-1].replace('""', '"')
-    return string
-
-
-class SubprocessError(RuntimeError):
-    "A subprocess exited with a nonzero status"
-    def __init__(self, args, status, stdout=None, stderr=None):
-        self.status = status
-        self.stdout = stdout
-        self.stderr = stderr
-        msg = '{args} exited with {status}'.format(args=args, status=status)
-        if stderr:
-            msg = '{msg}: {stderr}'.format(msg=msg, stderr=stderr)
-        super(SubprocessError, self).__init__(msg)
-
-
-class _SubprocessContextManager(object):
-    """
-    PEP 343 context manager for subprocesses.
-
-    'expect' holds a tuple of acceptable exit codes, otherwise we'll
-    raise a SubprocessError in __exit__.
-    """
-    def __init__(self, process, args, expect=(0,)):
-        self._process = process
-        self._args = args
-        self._expect = expect
-
-    def __enter__(self):
-        return self._process
-
-    def __exit__(self, type, value, traceback):
-        for name in ['stdin', 'stdout', 'stderr']:
-            stream = getattr(self._process, name)
-            if stream:
-                stream.close()
-                setattr(self._process, name, None)
-        status = self._process.wait()
-        _LOG.debug(
-            'collect {args} with status {status} (expected {expect})'.format(
-                args=self._args, status=status, expect=self._expect))
-        if status not in self._expect:
-            raise SubprocessError(args=self._args, status=status)
-
-    def wait(self):
-        return self._process.wait()
-
-
-def _spawn(args, input=None, additional_env=None, wait=False, stdin=None,
-           stdout=None, stderr=None, encoding=_locale.getpreferredencoding(),
-           expect=(0,), **kwargs):
-    """Spawn a subprocess, and optionally wait for it to finish.
-
-    This wrapper around subprocess.Popen has two modes, depending on
-    the truthiness of 'wait'.  If 'wait' is true, we use p.communicate
-    internally to write 'input' to the subprocess's stdin and read
-    from it's stdout/stderr.  If 'wait' is False, we return a
-    _SubprocessContextManager instance for fancier handling
-    (e.g. piping between processes).
-
-    For 'wait' calls when you want to write to the subprocess's stdin,
-    you only need to set 'input' to your content.  When 'input' is not
-    None but 'stdin' is, we'll automatically set 'stdin' to PIPE
-    before calling Popen.  This avoids having the subprocess
-    accidentally inherit the launching process's stdin.
-    """
-    _LOG.debug('spawn {args} (additional env. var.: {env})'.format(
-        args=args, env=additional_env))
-    if not stdin and input is not None:
-        stdin = _subprocess.PIPE
-    if additional_env:
-        if not kwargs.get('env'):
-            kwargs['env'] = dict(_os.environ)
-        kwargs['env'].update(additional_env)
-    p = _subprocess.Popen(
-        args, stdin=stdin, stdout=stdout, stderr=stderr, **kwargs)
-    if wait:
-        if hasattr(input, 'encode'):
-            input = input.encode(encoding)
-        (stdout, stderr) = p.communicate(input=input)
-        status = p.wait()
-        _LOG.debug(
-            'collect {args} with status {status} (expected {expect})'.format(
-                args=args, status=status, expect=expect))
-        if stdout is not None:
-            stdout = stdout.decode(encoding)
-        if stderr is not None:
-            stderr = stderr.decode(encoding)
-        if status not in expect:
-            raise SubprocessError(
-                args=args, status=status, stdout=stdout, stderr=stderr)
-        return (status, stdout, stderr)
-    if p.stdin and not stdin:
-        p.stdin.close()
-        p.stdin = None
-    if p.stdin:
-        p.stdin = _codecs.getwriter(encoding=encoding)(stream=p.stdin)
-    stream_reader = _codecs.getreader(encoding=encoding)
-    if p.stdout:
-        p.stdout = stream_reader(stream=p.stdout)
-    if p.stderr:
-        p.stderr = stream_reader(stream=p.stderr)
-    return _SubprocessContextManager(args=args, process=p, expect=expect)
-
-
-def _git(args, **kwargs):
-    args = ['git', '--git-dir', NMBGIT] + list(args)
-    return _spawn(args=args, **kwargs)
-
-
-def _get_current_branch():
-    """Get the name of the current branch.
-
-    Return 'None' if we're not on a branch.
-    """
-    try:
-        (status, branch, stderr) = _git(
-            args=['symbolic-ref', '--short', 'HEAD'],
-            stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True)
-    except SubprocessError as e:
-        if 'not a symbolic ref' in e:
-            return None
-        raise
-    return branch.strip()
-
-
-def _get_remote():
-    "Get the default remote for the current branch."
-    local_branch = _get_current_branch()
-    (status, remote, stderr) = _git(
-        args=['config', 'branch.{0}.remote'.format(local_branch)],
-        stdout=_subprocess.PIPE, wait=True)
-    return remote.strip()
-
-
-def get_tags(prefix=None):
-    "Get a list of tags with a given prefix."
-    if prefix is None:
-        prefix = TAG_PREFIX
-    (status, stdout, stderr) = _spawn(
-        args=['notmuch', 'search', '--output=tags', '*'],
-        stdout=_subprocess.PIPE, wait=True)
-    return [tag for tag in stdout.splitlines() if tag.startswith(prefix)]
-
-
-def archive(treeish='HEAD', args=()):
-    """
-    Dump a tar archive of the current nmbug tag set.
-
-    Using 'git archive'.
-
-    Each tag $tag for message with Message-Id $id is written to
-    an empty file
-
-      tags/encode($id)/encode($tag)
-
-    The encoding preserves alphanumerics, and the characters
-    "+-_@=.:," (not the quotes).  All other octets are replaced with
-    '%' followed by a two digit hex number.
-    """
-    _git(args=['archive', treeish] + list(args), wait=True)
-
-
-def clone(repository):
-    """
-    Create a local nmbug repository from a remote source.
-
-    This wraps 'git clone', adding some options to avoid creating a
-    working tree while preserving remote-tracking branches and
-    upstreams.
-    """
-    with _tempfile.TemporaryDirectory(prefix='nmbug-clone.') as workdir:
-        _spawn(
-            args=[
-                'git', 'clone', '--no-checkout', '--separate-git-dir', NMBGIT,
-                repository, workdir],
-            wait=True)
-    _git(args=['config', '--unset', 'core.worktree'], wait=True, expect=(0, 5))
-    _git(args=['config', 'core.bare', 'true'], wait=True)
-    _git(args=['branch', 'config', 'origin/config'], wait=True)
-    existing_tags = get_tags()
-    if existing_tags:
-        _LOG.warning(
-            'Not checking out to avoid clobbering existing tags: {}'.format(
-            ', '.join(existing_tags)))
-    else:
-        checkout()
-
-
-def _is_committed(status):
-    return len(status['added']) + len(status['deleted']) == 0
-
-
-def commit(treeish='HEAD', message=None):
-    """
-    Commit prefix-matching tags from the notmuch database to Git.
-    """
-    status = get_status()
-
-    if _is_committed(status=status):
-        _LOG.warning('Nothing to commit')
-        return
-
-    _git(args=['read-tree', '--empty'], wait=True)
-    _git(args=['read-tree', treeish], wait=True)
-    try:
-        _update_index(status=status)
-        (_, tree, _) = _git(
-            args=['write-tree'],
-            stdout=_subprocess.PIPE,
-            wait=True)
-        (_, parent, _) = _git(
-            args=['rev-parse', treeish],
-            stdout=_subprocess.PIPE,
-            wait=True)
-        (_, commit, _) = _git(
-            args=['commit-tree', tree.strip(), '-p', parent.strip()],
-            input=message,
-            stdout=_subprocess.PIPE,
-            wait=True)
-        _git(
-            args=['update-ref', treeish, commit.strip()],
-            stdout=_subprocess.PIPE,
-            wait=True)
-    except Exception as e:
-        _git(args=['read-tree', '--empty'], wait=True)
-        _git(args=['read-tree', treeish], wait=True)
-        raise
-
-def _update_index(status):
-    with _git(
-            args=['update-index', '--index-info'],
-            stdin=_subprocess.PIPE) as p:
-        for id, tags in status['deleted'].items():
-            for line in _index_tags_for_message(id=id, status='D', tags=tags):
-                p.stdin.write(line)
-        for id, tags in status['added'].items():
-            for line in _index_tags_for_message(id=id, status='A', tags=tags):
-                p.stdin.write(line)
-
-
-def fetch(remote=None):
-    """
-    Fetch changes from the remote repository.
-
-    See 'merge' to bring those changes into notmuch.
-    """
-    args = ['fetch']
-    if remote:
-        args.append(remote)
-    _git(args=args, wait=True)
-
-
-def checkout():
-    """
-    Update the notmuch database from Git.
-
-    This is mainly useful to discard your changes in notmuch relative
-    to Git.
-    """
-    status = get_status()
-    with _spawn(
-            args=['notmuch', 'tag', '--batch'], stdin=_subprocess.PIPE) as p:
-        for id, tags in status['added'].items():
-            p.stdin.write(_batch_line(action='-', id=id, tags=tags))
-        for id, tags in status['deleted'].items():
-            p.stdin.write(_batch_line(action='+', id=id, tags=tags))
-
-
-def _batch_line(action, id, tags):
-    """
-    'notmuch tag --batch' line for adding/removing tags.
-
-    Set 'action' to '-' to remove a tag or '+' to add the tags to a
-    given message id.
-    """
-    tag_string = ' '.join(
-        '{action}{prefix}{tag}'.format(
-            action=action, prefix=_ENCODED_TAG_PREFIX, tag=_hex_quote(tag))
-        for tag in tags)
-    line = '{tags} -- id:{id}\n'.format(
-        tags=tag_string, id=_xapian_quote(string=id))
-    return line
-
-
-def _insist_committed():
-    "Die if the the notmuch tags don't match the current HEAD."
-    status = get_status()
-    if not _is_committed(status=status):
-        _LOG.error('\n'.join([
-            'Uncommitted changes to {prefix}* tags in notmuch',
-            '',
-            "For a summary of changes, run 'nmbug status'",
-            "To save your changes,     run 'nmbug commit' before merging/pull",
-            "To discard your changes,  run 'nmbug checkout'",
-            ]).format(prefix=TAG_PREFIX))
-        _sys.exit(1)
-
-
-def pull(repository=None, refspecs=None):
-    """
-    Pull (merge) remote repository changes to notmuch.
-
-    'pull' is equivalent to 'fetch' followed by 'merge'.  We use the
-    Git-configured repository for your current branch
-    (branch.<name>.repository, likely 'origin', and
-    branch.<name>.merge, likely 'master').
-    """
-    _insist_committed()
-    if refspecs and not repository:
-        repository = _get_remote()
-    args = ['pull']
-    if repository:
-        args.append(repository)
-    if refspecs:
-        args.extend(refspecs)
-    with _tempfile.TemporaryDirectory(prefix='nmbug-pull.') as workdir:
-        for command in [
-                ['reset', '--hard'],
-                args]:
-            _git(
-                args=command,
-                additional_env={'GIT_WORK_TREE': workdir},
-                wait=True)
-    checkout()
-
-
-def merge(reference='@{upstream}'):
-    """
-    Merge changes from 'reference' into HEAD and load the result into notmuch.
-
-    The default reference is '@{upstream}'.
-    """
-    _insist_committed()
-    with _tempfile.TemporaryDirectory(prefix='nmbug-merge.') as workdir:
-        for command in [
-                ['reset', '--hard'],
-                ['merge', reference]]:
-            _git(
-                args=command,
-                additional_env={'GIT_WORK_TREE': workdir},
-                wait=True)
-    checkout()
-
-
-def log(args=()):
-    """
-    A simple wrapper for 'git log'.
-
-    After running 'nmbug fetch', you can inspect the changes with
-    'nmbug log HEAD..@{upstream}'.
-    """
-    # we don't want output trapping here, because we want the pager.
-    args = ['log', '--name-status', '--no-renames'] + list(args)
-    with _git(args=args, expect=(0, 1, -13)) as p:
-        p.wait()
-
-
-def push(repository=None, refspecs=None):
-    "Push the local nmbug Git state to a remote repository."
-    if refspecs and not repository:
-        repository = _get_remote()
-    args = ['push']
-    if repository:
-        args.append(repository)
-    if refspecs:
-        args.extend(refspecs)
-    _git(args=args, wait=True)
-
-
-def status():
-    """
-    Show pending updates in notmuch or git repo.
-
-    Prints lines of the form
-
-      ng Message-Id tag
-
-    where n is a single character representing notmuch database status
-
-    * A
-
-      Tag is present in notmuch database, but not committed to nmbug
-      (equivalently, tag has been deleted in nmbug repo, e.g. by a
-      pull, but not restored to notmuch database).
-
-    * D
-
-      Tag is present in nmbug repo, but not restored to notmuch
-      database (equivalently, tag has been deleted in notmuch).
-
-    * U
-
-      Message is unknown (missing from local notmuch database).
-
-    The second character (if present) represents a difference between
-    local and upstream branches. Typically 'nmbug fetch' needs to be
-    run to update this.
-
-    * a
-
-      Tag is present in upstream, but not in the local Git branch.
-
-    * d
-
-      Tag is present in local Git branch, but not upstream.
-    """
-    status = get_status()
-    # 'output' is a nested defaultdict for message status:
-    # * The outer dict is keyed by message id.
-    # * The inner dict is keyed by tag name.
-    # * The inner dict values are status strings (' a', 'Dd', ...).
-    output = _collections.defaultdict(
-        lambda : _collections.defaultdict(lambda : ' '))
-    for id, tags in status['added'].items():
-        for tag in tags:
-            output[id][tag] = 'A'
-    for id, tags in status['deleted'].items():
-        for tag in tags:
-            output[id][tag] = 'D'
-    for id, tags in status['missing'].items():
-        for tag in tags:
-            output[id][tag] = 'U'
-    if _is_unmerged():
-        for id, tag in _diff_refs(filter='A'):
-            output[id][tag] += 'a'
-        for id, tag in _diff_refs(filter='D'):
-            output[id][tag] += 'd'
-    for id, tag_status in sorted(output.items()):
-        for tag, status in sorted(tag_status.items()):
-            print('{status}\t{id}\t{tag}'.format(
-                status=status, id=id, tag=tag))
-
-
-def _is_unmerged(ref='@{upstream}'):
-    try:
-        (status, fetch_head, stderr) = _git(
-            args=['rev-parse', ref],
-            stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True)
-    except SubprocessError as e:
-        if 'No upstream configured' in e.stderr:
-            return
-        raise
-    (status, base, stderr) = _git(
-        args=['merge-base', 'HEAD', ref],
-        stdout=_subprocess.PIPE, wait=True)
-    return base != fetch_head
-
-
-def get_status():
-    status = {
-        'deleted': {},
-        'missing': {},
-        }
-    index = _index_tags()
-    maybe_deleted = _diff_index(index=index, filter='D')
-    for id, tags in maybe_deleted.items():
-        (_, stdout, stderr) = _spawn(
-            args=['notmuch', 'search', '--output=files', 'id:{0}'.format(id)],
-            stdout=_subprocess.PIPE,
-            wait=True)
-        if stdout:
-            status['deleted'][id] = tags
-        else:
-            status['missing'][id] = tags
-    status['added'] = _diff_index(index=index, filter='A')
-    _os.remove(index)
-    return status
-
-
-def _index_tags():
-    "Write notmuch tags to the nmbug.index."
-    path = _os.path.join(NMBGIT, 'nmbug.index')
-    query = ' '.join('tag:"{tag}"'.format(tag=tag) for tag in get_tags())
-    prefix = '+{0}'.format(_ENCODED_TAG_PREFIX)
-    _git(
-        args=['read-tree', '--empty'],
-        additional_env={'GIT_INDEX_FILE': path}, wait=True)
-    with _spawn(
-            args=['notmuch', 'dump', '--format=batch-tag', '--', query],
-            stdout=_subprocess.PIPE) as notmuch:
-        with _git(
-                args=['update-index', '--index-info'],
-                stdin=_subprocess.PIPE,
-                additional_env={'GIT_INDEX_FILE': path}) as git:
-            for line in notmuch.stdout:
-                if line.strip().startswith('#'):
-                    continue
-                (tags_string, id) = [_.strip() for _ in line.split(' -- id:')]
-                tags = [
-                    _unquote(tag[len(prefix):])
-                    for tag in tags_string.split()
-                    if tag.startswith(prefix)]
-                id = _xapian_unquote(string=id)
-                for line in _index_tags_for_message(
-                        id=id, status='A', tags=tags):
-                    git.stdin.write(line)
-    return path
-
-
-def _index_tags_for_message(id, status, tags):
-    """
-    Update the Git index to either create or delete an empty file.
-
-    Neither 'id' nor the tags in 'tags' should be encoded/escaped.
-    """
-    mode = '100644'
-    hash = _EMPTYBLOB
-
-    if status == 'D':
-        mode = '0'
-        hash = '0000000000000000000000000000000000000000'
-
-    for tag in tags:
-        path = 'tags/{id}/{tag}'.format(
-            id=_hex_quote(string=id), tag=_hex_quote(string=tag))
-        yield '{mode} {hash}\t{path}\n'.format(mode=mode, hash=hash, path=path)
-
-
-def _diff_index(index, filter):
-    """
-    Get an {id: {tag, ...}} dict for a given filter.
-
-    For example, use 'A' to find added tags, and 'D' to find deleted tags.
-    """
-    s = _collections.defaultdict(set)
-    with _git(
-            args=[
-                'diff-index', '--cached', '--diff-filter', filter,
-                '--name-only', 'HEAD'],
-            additional_env={'GIT_INDEX_FILE': index},
-            stdout=_subprocess.PIPE) as p:
-        # Once we drop Python < 3.3, we can use 'yield from' here
-        for id, tag in _unpack_diff_lines(stream=p.stdout):
-            s[id].add(tag)
-    return s
-
-
-def _diff_refs(filter, a='HEAD', b='@{upstream}'):
-    with _git(
-            args=['diff', '--diff-filter', filter, '--name-only', a, b],
-            stdout=_subprocess.PIPE) as p:
-        # Once we drop Python < 3.3, we can use 'yield from' here
-        for id, tag in _unpack_diff_lines(stream=p.stdout):
-            yield id, tag
-
-
-def _unpack_diff_lines(stream):
-    "Iterate through (id, tag) tuples in a diff stream."
-    for line in stream:
-        match = _TAG_FILE_REGEX.match(line.strip())
-        if not match:
-            message = 'non-tag line in diff: {!r}'.format(line.strip())
-            if line.startswith(_TAG_DIRECTORY):
-                raise ValueError(message)
-            _LOG.info(message)
-            continue
-        id = _unquote(match.group('id'))
-        tag = _unquote(match.group('tag'))
-        yield (id, tag)
-
-
-def _help(parser, command=None):
-    """
-    Show help for an nmbug command.
-
-    Because some folks prefer:
-
-      $ nmbug help COMMAND
-
-    to
-
-      $ nmbug COMMAND --help
-    """
-    if command:
-        parser.parse_args([command, '--help'])
-    else:
-        parser.parse_args(['--help'])
-
-
-if __name__ == '__main__':
-    import argparse
-
-    parser = argparse.ArgumentParser(
-        description=__doc__.strip(),
-        formatter_class=argparse.RawDescriptionHelpFormatter)
-    parser.add_argument(
-        '-v', '--version', action='version',
-        version='%(prog)s {}'.format(__version__))
-    parser.add_argument(
-        '-l', '--log-level',
-        choices=['critical', 'error', 'warning', 'info', 'debug'],
-        help='Log verbosity.  Defaults to {!r}.'.format(
-            _logging.getLevelName(_LOG.level).lower()))
-
-    help = _functools.partial(_help, parser=parser)
-    help.__doc__ = _help.__doc__
-    subparsers = parser.add_subparsers(
-        title='commands',
-        description=(
-            'For help on a particular command, run: '
-            "'%(prog)s ... <command> --help'."))
-    for command in [
-            'archive',
-            'checkout',
-            'clone',
-            'commit',
-            'fetch',
-            'help',
-            'log',
-            'merge',
-            'pull',
-            'push',
-            'status',
-            ]:
-        func = locals()[command]
-        doc = _textwrap.dedent(func.__doc__).strip().replace('%', '%%')
-        subparser = subparsers.add_parser(
-            command,
-            help=doc.splitlines()[0],
-            description=doc,
-            formatter_class=argparse.RawDescriptionHelpFormatter)
-        subparser.set_defaults(func=func)
-        if command == 'archive':
-            subparser.add_argument(
-                'treeish', metavar='TREE-ISH', nargs='?', default='HEAD',
-                help=(
-                    'The tree or commit to produce an archive for.  Defaults '
-                    "to 'HEAD'."))
-            subparser.add_argument(
-                'args', metavar='ARG', nargs='*',
-                help=(
-                    "Argument passed through to 'git archive'.  Set anything "
-                    'before <tree-ish>, see git-archive(1) for details.'))
-        elif command == 'clone':
-            subparser.add_argument(
-                'repository',
-                help=(
-                    'The (possibly remote) repository to clone from.  See the '
-                    'URLS section of git-clone(1) for more information on '
-                    'specifying repositories.'))
-        elif command == 'commit':
-            subparser.add_argument(
-                'message', metavar='MESSAGE', default='', nargs='?',
-                help='Text for the commit message.')
-        elif command == 'fetch':
-            subparser.add_argument(
-                'remote', metavar='REMOTE', nargs='?',
-                help=(
-                    'Override the default configured in branch.<name>.remote '
-                    'to fetch from a particular remote repository (e.g. '
-                    "'origin')."))
-        elif command == 'help':
-            subparser.add_argument(
-                'command', metavar='COMMAND', nargs='?',
-                help='The command to show help for.')
-        elif command == 'log':
-            subparser.add_argument(
-                'args', metavar='ARG', nargs='*',
-                help="Additional argument passed through to 'git log'.")
-        elif command == 'merge':
-            subparser.add_argument(
-                'reference', metavar='REFERENCE', default='@{upstream}',
-                nargs='?',
-                help=(
-                    'Reference, usually other branch heads, to merge into '
-                    "our branch.  Defaults to '@{upstream}'."))
-        elif command == 'pull':
-            subparser.add_argument(
-                'repository', metavar='REPOSITORY', default=None, nargs='?',
-                help=(
-                    'The "remote" repository that is the source of the pull.  '
-                    'This parameter can be either a URL (see the section GIT '
-                    'URLS in git-pull(1)) or the name of a remote (see the '
-                    'section REMOTES in git-pull(1)).'))
-            subparser.add_argument(
-                'refspecs', metavar='REFSPEC', default=None, nargs='*',
-                help=(
-                    'Refspec (usually a branch name) to fetch and merge.  See '
-                    'the <refspec> entry in the OPTIONS section of '
-                    'git-pull(1) for other possibilities.'))
-        elif command == 'push':
-            subparser.add_argument(
-               'repository', metavar='REPOSITORY', default=None, nargs='?',
-                help=(
-                    'The "remote" repository that is the destination of the '
-                    'push.  This parameter can be either a URL (see the '
-                    'section GIT URLS in git-push(1)) or the name of a remote '
-                    '(see the section REMOTES in git-push(1)).'))
-            subparser.add_argument(
-                'refspecs', metavar='REFSPEC', default=None, nargs='*',
-                help=(
-                    'Refspec (usually a branch name) to push.  See '
-                    'the <refspec> entry in the OPTIONS section of '
-                    'git-push(1) for other possibilities.'))
-
-    args = parser.parse_args()
-
-    if args.log_level:
-        level = getattr(_logging, args.log_level.upper())
-        _LOG.setLevel(level)
-
-    if not getattr(args, 'func', None):
-        parser.print_usage()
-        _sys.exit(1)
-
-    if args.func == help:
-        arg_names = ['command']
-    else:
-        (arg_names, varargs, varkw) = _inspect.getargs(args.func.__code__)
-    kwargs = {key: getattr(args, key) for key in arg_names if key in args}
-    try:
-        args.func(**kwargs)
-    except SubprocessError as e:
-        if _LOG.level == _logging.DEBUG:
-            raise  # don't mask the traceback
-        _LOG.error(str(e))
-        _sys.exit(1)
index 01810888e5a62d0ee28569cd5406ba19cefb7050..66bcdbed8d0146967b10260bfb6a599cc3bb63b0 100644 (file)
@@ -83,6 +83,7 @@ message = {
 
     headers:        headers,
     crypto:         crypto,
+    duplicate:      integer,
     body?:          [part]    # omitted if --body=false
 }
 
index d43ef26923bcd9f192047ff2f5d5b1c101a62d0c..51c729cf7fe5d61ef5719e25c797ecff62d40af5 100644 (file)
@@ -18,7 +18,9 @@ MAN7_RST := $(wildcard $(srcdir)/doc/man7/*.rst)
 MAN_RST_FILES := $(MAN1_RST) $(MAN5_RST) $(MAN7_RST)
 ALL_RST_FILES := $(MAN_RST_FILES) $(srcdir)/doc/notmuch-emacs.rst
 
+COPY_ROFF1 := $(patsubst %,$(DOCBUILDDIR)/man/man1/%.1,nmbug notmuch-setup)
 MAN1_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN1_RST:.rst=.1))
+MAN1_ROFF := $(MAN1_ROFF) $(COPY_ROFF1)
 MAN5_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN5_RST:.rst=.5))
 MAN7_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN7_RST:.rst=.7))
 MAN_ROFF_FILES := $(MAN1_ROFF) $(MAN5_ROFF) $(MAN7_ROFF)
@@ -33,7 +35,8 @@ ifeq ($(WITH_EMACS),1)
        INFO_TEXI_FILES += $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi
 endif
 
-INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info)
+COPY_INFO1 := $(patsubst $(DOCBUILDDIR)/man/man1/%.1,$(DOCBUILDDIR)/texinfo/%.info,$(COPY_ROFF1))
+INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info) $(COPY_INFO1)
 
 .PHONY: sphinx-html sphinx-texinfo sphinx-info
 
@@ -130,7 +133,6 @@ install-man: ${MAN_GZIP_FILES}
        install -m0644 $(filter %.1.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man1
        install -m0644 $(filter %.5.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man5
        install -m0644 $(filter %.7.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man7
-       cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch.1.gz notmuch-setup.1.gz
 endif
 
 ifneq ($(HAVE_SPHINX)$(HAVE_MAKEINFO),11)
diff --git a/doc/command-line.rst b/doc/command-line.rst
new file mode 100644 (file)
index 0000000..543a5f9
--- /dev/null
@@ -0,0 +1,36 @@
+Notmuch Command Line Interface
+==============================
+
+Main commands
+-------------
+
+.. toctree::
+   :titlesonly:
+
+   man1/notmuch
+   man1/notmuch-address
+   man1/notmuch-compact
+   man1/notmuch-config
+   man1/notmuch-count
+   man1/notmuch-dump
+   man1/notmuch-emacs-mua
+   man1/notmuch-git
+   man1/notmuch-insert
+   man1/notmuch-new
+   man1/notmuch-reindex
+   man1/notmuch-reply
+   man1/notmuch-restore
+   man1/notmuch-search
+   man1/notmuch-show
+   man1/notmuch-tag
+   man5/notmuch-hooks
+
+Aliases
+-------
+  
+.. toctree::
+   :titlesonly:
+
+   nmbug <man1/notmuch-git>
+   notmuch-setup <man1/notmuch>
+   
index e46e1d4e47faa089c45553f747a6f95003dba9a4..e23cb7d7fb444964cb341269771629713f8c2d2e 100644 (file)
@@ -3,8 +3,10 @@
 
 import sys
 import os
+from pathlib import Path
+sys.path.append(str(Path(__file__).parent))
 
-extensions = [ 'sphinx.ext.autodoc' ]
+extensions = [ 'sphinx.ext.autodoc', 'elisp' ]
 
 # The suffix of source filenames.
 source_suffix = '.rst'
@@ -45,7 +47,7 @@ if tags.has('WITH_EMACS'):
     # Hacky reimplementation of include to workaround limitations of
     # sphinx-doc
     lines = ['.. include:: /../emacs/rstdoc.rsti\n\n'] # in the source tree
-    for file in ('notmuch.rsti', 'notmuch-lib.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti', 'notmuch-tree.rsti'):
+    for file in ('notmuch.rsti', 'notmuch-lib.rsti', 'notmuch-hello.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti', 'notmuch-tree.rsti'):
         lines.extend(open(rsti_dir+'/'+file))
     rst_epilog = ''.join(lines)
     del lines
@@ -67,6 +69,8 @@ pygments_style = 'sphinx'
 # a list of builtin themes.
 html_theme = 'default'
 
+# prevent generation of python module index
+html_domain_indices=[]
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -123,6 +127,14 @@ man_pages = [
      u'send mail with notmuch and emacs',
      [notmuch_authors], 1),
 
+    ('man1/notmuch-git', 'notmuch-git',
+     u'manage notmuch tags with git',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-git', 'nmbug',
+     u'manage notmuch bugs with git',
+     [notmuch_authors], 1),
+
     ('man5/notmuch-hooks', 'notmuch-hooks',
      u'hooks for notmuch',
      [notmuch_authors], 5),
@@ -159,6 +171,10 @@ man_pages = [
      u'syntax for notmuch queries',
      [notmuch_authors], 7),
 
+    ('man1/notmuch', 'notmuch-setup',
+     u'getting started with notmuch',
+     [notmuch_authors], 1),
+
     ('man7/notmuch-sexp-queries', 'notmuch-sexp-queries',
      u's-expression syntax for notmuch queries',
      [notmuch_authors], 7),
@@ -200,3 +216,11 @@ texinfo_documents += [
         x[2],                          # description
         'Miscellaneous'                        # category
     ) for x in man_pages]
+
+def setup(app):
+    import docutils.nodes
+    # define nmconfig role and directive for config items.
+    app.add_object_type('nmconfig','nmconfig',
+                        indextemplate='pair: configuration item; %s',
+                        ref_nodeclass=docutils.nodes.generated,
+                        objname='config item' )
diff --git a/doc/elisp.py b/doc/elisp.py
new file mode 100644 (file)
index 0000000..1b0392e
--- /dev/null
@@ -0,0 +1,445 @@
+# Copyright (C) 2016 Sebastian Wiesner and Flycheck contributors
+
+# This file is not part of GNU Emacs.
+
+# 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/>.
+
+
+from collections import namedtuple
+from sphinx import addnodes
+from sphinx.util import ws_re
+from sphinx.roles import XRefRole
+from sphinx.domains import Domain, ObjType
+from sphinx.util.nodes import make_refnode
+from sphinx.directives import ObjectDescription
+
+
+def make_target(cell, name):
+    """Create a target name from ``cell`` and ``name``.
+
+    ``cell`` is the name of a symbol cell, and ``name`` is a symbol name, both
+    as strings.
+
+    The target names are used as cross-reference targets for Sphinx.
+
+    """
+    return '{cell}-{name}'.format(cell=cell, name=name)
+
+
+def to_mode_name(symbol_name):
+    """Convert ``symbol_name`` to a mode name.
+
+    Split at ``-`` and titlecase each part.
+
+    """
+    return ' '.join(p.title() for p in symbol_name.split('-'))
+
+
+class Cell(namedtuple('Cell', 'objtype docname')):
+    """A cell in a symbol.
+
+    A cell holds the object type and the document name of the description for
+    the cell.
+
+    Cell objects are used within symbol entries in the domain data.
+
+    """
+
+    pass
+
+
+class KeySequence(namedtuple('KeySequence', 'keys')):
+    """A key sequence."""
+
+    PREFIX_KEYS = {'C-u'}
+    PREFIX_KEYS.update('M-{}'.format(n) for n in range(10))
+
+    @classmethod
+    def fromstring(cls, s):
+        return cls(s.split())
+
+    @property
+    def command_name(self):
+        """The command name in this key sequence.
+
+        Return ``None`` for key sequences that are no command invocations with
+        ``M-x``.
+
+        """
+        try:
+            return self.keys[self.keys.index('M-x') + 1]
+        except ValueError:
+            return None
+
+    @property
+    def has_prefix(self):
+        """Whether this key sequence has a prefix."""
+        return self.keys[0] in self.PREFIX_KEYS
+
+    def __str__(self):
+        return ' '.join(self.keys)
+
+
+class EmacsLispSymbol(ObjectDescription):
+    """An abstract base class for directives documenting symbols.
+
+    Provide target and index generation and registration of documented symbols
+    within the domain data.
+
+    Deriving classes must have a ``cell`` attribute which refers to the cell
+    the documentation goes in, and a ``label`` attribute which provides a
+    human-readable name for what is documented, used in the index entry.
+
+    """
+
+    cell_for_objtype = {
+        'defcustom': 'variable',
+        'defconst': 'variable',
+        'defvar': 'variable',
+        'defface': 'face'
+    }
+
+    category_for_objtype = {
+        'defcustom': 'Emacs variable (customizable)',
+        'defconst': 'Emacs constant',
+        'defvar': 'Emacs variable',
+        'defface': 'Emacs face'
+    }
+
+    @property
+    def cell(self):
+        """The cell in which to store symbol metadata."""
+        return self.cell_for_objtype[self.objtype]
+
+    @property
+    def label(self):
+        """The label for the documented object type."""
+        return self.objtype
+
+    @property
+    def category(self):
+        """Index category"""
+        return self.category_for_objtype[self.objtype]
+
+    def handle_signature(self, signature, signode):
+        """Create nodes in ``signode`` for the ``signature``.
+
+        ``signode`` is a docutils node to which to add the nodes, and
+        ``signature`` is the symbol name.
+
+        Add the object type label before the symbol name and return
+        ``signature``.
+
+        """
+        label = self.label + ' '
+        signode += addnodes.desc_annotation(label, label)
+        signode += addnodes.desc_name(signature, signature)
+        return signature
+
+    def _add_index(self, name, target):
+        index_text = '{name}; {label}'.format(
+            name=name, label=self.category)
+        self.indexnode['entries'].append(
+            ('pair', index_text, target, '', None))
+
+    def _add_target(self, name, sig, signode):
+        target = make_target(self.cell, name)
+        if target not in self.state.document.ids:
+            signode['names'].append(name)
+            signode['ids'].append(target)
+            signode['first'] = (not self.names)
+            self.state.document.note_explicit_target(signode)
+
+            obarray = self.env.domaindata['el']['obarray']
+            symbol = obarray.setdefault(name, {})
+            if self.cell in symbol:
+                self.state_machine.reporter.warning(
+                    'duplicate description of %s %s, ' % (self.objtype, name)
+                    + 'other instance in '
+                    + self.env.doc2path(symbol[self.cell].docname),
+                    line=self.lineno)
+            symbol[self.cell] = Cell(self.objtype, self.env.docname)
+
+        return target
+
+    def add_target_and_index(self, name, sig, signode):
+        target = self._add_target(name, sig, signode)
+        self._add_index(name, target)
+
+
+class EmacsLispMinorMode(EmacsLispSymbol):
+    cell = 'function'
+    label = 'Minor Mode'
+
+    def handle_signature(self, signature, signode):
+        """Create nodes in ``signode`` for the ``signature``.
+
+        ``signode`` is a docutils node to which to add the nodes, and
+        ``signature`` is the symbol name.
+
+        Add the object type label before the symbol name and return
+        ``signature``.
+
+        """
+        label = self.label + ' '
+        signode += addnodes.desc_annotation(label, label)
+        signode += addnodes.desc_name(signature, to_mode_name(signature))
+        return signature
+
+    def _add_index(self, name, target):
+        return super()._add_index(to_mode_name(name), target)
+
+
+class EmacsLispFunction(EmacsLispSymbol):
+    """A directive to document Emacs Lisp functions."""
+
+    cell_for_objtype = {
+        'defun': 'function',
+        'defmacro': 'function'
+    }
+
+    def handle_signature(self, signature, signode):
+        function_name, *args = ws_re.split(signature)
+        label = self.label + ' '
+        signode += addnodes.desc_annotation(label, label)
+        signode += addnodes.desc_name(function_name, function_name)
+        for arg in args:
+            is_keyword = arg.startswith('&')
+            node = (addnodes.desc_annotation
+                    if is_keyword
+                    else addnodes.desc_addname)
+            signode += node(' ' + arg, ' ' + arg)
+
+        return function_name
+
+
+class EmacsLispKey(ObjectDescription):
+    """A directive to document interactive commands via their bindings."""
+
+    label = 'Emacs command'
+
+    def handle_signature(self, signature, signode):
+        """Create nodes to ``signode`` for ``signature``.
+
+        ``signode`` is a docutils node to which to add the nodes, and
+        ``signature`` is the symbol name.
+        """
+        key_sequence = KeySequence.fromstring(signature)
+        signode += addnodes.desc_name(signature, str(key_sequence))
+        return str(key_sequence)
+
+    def _add_command_target_and_index(self, name, sig, signode):
+        target_name = make_target('function', name)
+        if target_name not in self.state.document.ids:
+            signode['names'].append(name)
+            signode['ids'].append(target_name)
+            self.state.document.note_explicit_target(signode)
+
+            obarray = self.env.domaindata['el']['obarray']
+            symbol = obarray.setdefault(name, {})
+            if 'function' in symbol:
+                self.state_machine.reporter.warning(
+                    'duplicate description of %s %s, ' % (self.objtype, name)
+                    + 'other instance in '
+                    + self.env.doc2path(symbol['function'].docname),
+                    line=self.lineno)
+            symbol['function'] = Cell(self.objtype, self.env.docname)
+
+        index_text = '{name}; {label}'.format(name=name, label=self.label)
+        self.indexnode['entries'].append(
+            ('pair', index_text, target_name, '', None))
+
+    def _add_binding_target_and_index(self, binding, sig, signode):
+        reftarget = make_target('key', binding)
+
+        if reftarget not in self.state.document.ids:
+            signode['names'].append(reftarget)
+            signode['ids'].append(reftarget)
+            signode['first'] = (not self.names)
+            self.state.document.note_explicit_target(signode)
+
+            keymap = self.env.domaindata['el']['keymap']
+            if binding in keymap:
+                self.state_machine.reporter.warning(
+                    'duplicate description of binding %s, ' % binding
+                    + 'other instance in '
+                    + self.env.doc2path(keymap[binding]),
+                    line=self.lineno)
+            keymap[binding] = self.env.docname
+
+        index_text = '{name}; Emacs key binding'.format(name=binding)
+        self.indexnode['entries'].append(
+            ('pair', index_text, reftarget, '', None))
+
+    def add_target_and_index(self, name, sig, signode):
+        # If unprefixed M-x command index as function and not as key binding
+        sequence = KeySequence.fromstring(name)
+        if sequence.command_name and not sequence.has_prefix:
+            self._add_command_target_and_index(sequence.command_name,
+                                               sig, signode)
+        else:
+            self._add_binding_target_and_index(name, sig, signode)
+
+
+class XRefModeRole(XRefRole):
+    """A role to cross-reference a minor mode.
+
+    Like a normal cross-reference role but appends ``-mode`` to the reference
+    target and title-cases the symbol name like Emacs does when referring to
+    modes.
+
+    """
+
+    fix_parens = False
+    lowercase = False
+
+    def process_link(self, env, refnode, has_explicit_title, title, target):
+        refnode['reftype'] = 'minor-mode'
+        target = target + '-mode'
+        return (title if has_explicit_title else to_mode_name(target), target)
+
+
+class EmacsLispDomain(Domain):
+    """A domain to document Emacs Lisp code."""
+
+    name = 'el'
+    label = ''
+
+    object_types = {
+        # TODO: Set search prio for object types
+        # Types for user-facing options and commands
+        'minor-mode': ObjType('minor-mode', 'function', 'mode',
+                              cell='function'),
+        'define-key': ObjType('key binding', cell='interactive'),
+        'defcustom': ObjType('defcustom', 'defcustom', cell='variable'),
+        'defface': ObjType('defface', 'defface', cell='face'),
+        # Object types for code
+        'defun': ObjType('defun', 'defun', cell='function'),
+        'defmacro': ObjType('defmacro', 'defmacro', cell='function'),
+        'defvar': ObjType('defvar', 'defvar', cell='variable'),
+        'defconst': ObjType('defconst', 'defconst', cell='variable')
+    }
+    directives = {
+        'minor-mode': EmacsLispMinorMode,
+        'define-key': EmacsLispKey,
+        'defcustom': EmacsLispSymbol,
+        'defvar': EmacsLispSymbol,
+        'defconst': EmacsLispSymbol,
+        'defface': EmacsLispSymbol,
+        'defun': EmacsLispFunction,
+        'defmacro': EmacsLispFunction
+    }
+    roles = {
+        'mode': XRefModeRole(),
+        'defvar': XRefRole(),
+        'defconst': XRefRole(),
+        'defcustom': XRefRole(),
+        'defface': XRefRole(),
+        'defun': XRefRole(),
+        'defmacro': XRefRole()
+    }
+
+    data_version = 1
+    initial_data = {
+        # Our domain data attempts to somewhat mirror the semantics of Emacs
+        # Lisp, so we have an obarray which holds symbols which in turn have
+        # function, variable, face, etc. cells, and a keymap which holds the
+        # documentation for key bindings.
+        'obarray': {},
+        'keymap': {}
+    }
+
+    def clear_doc(self, docname):
+        """Clear all cells documented ``docname``."""
+        for symbol in self.data['obarray'].values():
+            for cell in list(symbol.keys()):
+                if docname == symbol[cell].docname:
+                    del symbol[cell]
+        for binding in list(self.data['keymap']):
+            if self.data['keymap'][binding] == docname:
+                del self.data['keymap'][binding]
+
+    def resolve_xref(self, env, fromdocname, builder,
+                     objtype, target, node, contnode):
+        """Resolve a cross reference to ``target``."""
+        if objtype == 'key':
+            todocname = self.data['keymap'].get(target)
+            if not todocname:
+                return None
+            reftarget = make_target('key', target)
+        else:
+            cell = self.object_types[objtype].attrs['cell']
+            symbol = self.data['obarray'].get(target, {})
+            if cell not in symbol:
+                return None
+            reftarget = make_target(cell, target)
+            todocname = symbol[cell].docname
+
+        return make_refnode(builder, fromdocname, todocname,
+                            reftarget, contnode, target)
+
+    def resolve_any_xref(self, env, fromdocname, builder,
+                         target, node, contnode):
+        """Return all possible cross references for ``target``."""
+        nodes = ((objtype, self.resolve_xref(env, fromdocname, builder,
+                                             objtype, target, node, contnode))
+                 for objtype in ['key', 'defun', 'defvar', 'defface'])
+        return [('el:{}'.format(objtype), node) for (objtype, node) in nodes
+                if node is not None]
+
+    def merge_warn_duplicate(self, objname, our_docname, their_docname):
+        self.env.warn(
+            their_docname,
+            "Duplicate declaration: '{}' also defined in '{}'.\n".format(
+                objname, their_docname))
+
+    def merge_keymapdata(self, docnames, our_keymap, their_keymap):
+        for key, docname in their_keymap.items():
+            if docname in docnames:
+                if key in our_keymap:
+                    our_docname = our_keymap[key]
+                    self.merge_warn_duplicate(key, our_docname, docname)
+                else:
+                    our_keymap[key] = docname
+
+    def merge_obarraydata(self, docnames, our_obarray, their_obarray):
+        for objname, their_cells in their_obarray.items():
+            our_cells = our_obarray.setdefault(objname, dict())
+            for cellname, their_cell in their_cells.items():
+                if their_cell.docname in docnames:
+                    our_cell = our_cells.get(cellname)
+                    if our_cell:
+                        self.merge_warn_duplicate(objname, our_cell.docname,
+                                                  their_cell.docname)
+                    else:
+                        our_cells[cellname] = their_cell
+
+    def merge_domaindata(self, docnames, otherdata):
+        self.merge_keymapdata(docnames, self.data['keymap'],
+                              otherdata['keymap'])
+        self.merge_obarraydata(docnames, self.data['obarray'],
+                               otherdata['obarray'])
+
+    def get_objects(self):
+        """Get all documented symbols for use in the search index."""
+        for name, symbol in self.data['obarray'].items():
+            for cellname, cell in symbol.items():
+                yield (name, name, cell.objtype, cell.docname,
+                       make_target(cellname, name),
+                       self.object_types[cell.objtype].attrs['searchprio'])
+
+
+def setup(app):
+    app.add_domain(EmacsLispDomain)
+    return {'version': '0.1', 'parallel_read_safe': True}
index fbdcf779bdcacc300bc41c73aafebebbb71f8946..fec3e6c2d63f048e54c0aa55bd7c7a9d246af37d 100644 (file)
@@ -2,36 +2,19 @@
 Welcome to notmuch's documentation!
 ===================================
 
-Contents:
+Content
+-------
 
 .. toctree::
    :titlesonly:
 
-   man1/notmuch
-   man1/notmuch-address
-   man1/notmuch-compact
-   man1/notmuch-config
-   man1/notmuch-count
-   man1/notmuch-dump
+   command-line
+   queries
    notmuch-emacs
-   man1/notmuch-emacs-mua
-   man5/notmuch-hooks
-   man1/notmuch-insert
-   man1/notmuch-new
-   man7/notmuch-properties
-   man1/notmuch-reindex
-   man1/notmuch-reply
-   man1/notmuch-restore
-   man1/notmuch-search
-   man7/notmuch-search-terms
-   man7/notmuch-sexp-queries
-   man1/notmuch-show
-   man1/notmuch-tag
    python-bindings
-
-Indices and tables
-==================
+   
+Index and search
+-----------------
 
 * :ref:`genindex`
-* :ref:`modindex`
 * :ref:`search`
index acc5ecec6b01282c30d7b9bd821fde9ac1e8e2bf..388315f67ec609c396ee0168c7e9741b9990bbec 100644 (file)
@@ -55,13 +55,14 @@ The available configuration items are described below. Non-absolute
 paths are presumed relative to `$HOME` for items in section
 **database**.
 
-built_with.<name>
+.. nmconfig:: built_with.<name>
+
     Compile time feature <name>. Current possibilities include
     "retry_lock" (configure option, included by default).
     (since notmuch 0.30, "compact" and "field_processor" are
     always included.)
 
-database.autocommit
+.. nmconfig:: database.autocommit
 
     How often to commit transactions to disk. `0` means wait until
     command completes, otherwise an integer `n` specifies to commit to
@@ -69,7 +70,8 @@ database.autocommit
 
     History: this configuration value was introduced in notmuch 0.33.
 
-database.backup_dir
+.. nmconfig:: database.backup_dir
+
     Directory to store tag dumps when upgrading database.
 
     History: this configuration value was introduced in notmuch 0.32.
@@ -77,7 +79,8 @@ database.backup_dir
     Default: A sibling directory of the Xapian database called
     `backups`.
 
-database.hook_dir
+.. nmconfig:: database.hook_dir
+
     Directory containing hooks run by notmuch commands. See
     :any:`notmuch-hooks(5)`.
 
@@ -85,9 +88,8 @@ database.hook_dir
 
     Default: See HOOKS, below.
 
-.. _database.mail_root:
+.. nmconfig:: database.mail_root
 
-database.mail_root
     The top-level directory where your mail currently exists and to
     where mail will be delivered in the future. Files should be
     individual email messages.
@@ -95,18 +97,33 @@ database.mail_root
     History: this configuration value was introduced in notmuch 0.32.
 
     Default: For compatibility with older configurations, the value of
-    database.path is used if **database.mail\_root** is unset.
+    database.path is used if :nmconfig:`database.mail_root` is unset.
+
+.. nmconfig:: database.path
 
-database.path
     Notmuch will store its database here, (in
-    sub-directory named ``.notmuch`` if **database.mail\_root**
+    sub-directory named ``.notmuch`` if :nmconfig:`database.mail_root`
     is unset).
 
     Default: see :ref:`database`
 
-.. _index.decrypt:
+.. nmconfig:: git.path
+
+    Default location for git repository for :any:`notmuch-git`.
+
+.. nmconfig:: git.safe_fraction
+
+   Some :any:`notmuch-git` operations check that the fraction of
+   messages changed (in the database or in git, as appropriate) is not
+   too large. This item controls what fraction of total messages is
+   considered "not too large".
+
+.. nmconfig:: git.tag_prefix
+
+    Default tag prefix (filter) for :any:`notmuch-git`.
+
+.. nmconfig:: index.decrypt
 
-index.decrypt
     Policy for decrypting encrypted messages during indexing.  Must be
     one of: ``false``, ``auto``, ``nostash``, or ``true``.
 
@@ -161,7 +178,8 @@ index.decrypt
 
 .. _index.header:
 
-index.header.<prefix>
+.. nmconfig:: index.header.<prefix>
+
     Define the query prefix <prefix>, based on a mail header. For
     example ``index.header.List=List-Id`` will add a probabilistic
     prefix ``List:`` that searches the ``List-Id`` field.  User
@@ -170,9 +188,8 @@ index.header.<prefix>
     supported. See :any:`notmuch-search-terms(7)` for a list of existing
     prefixes, and an explanation of probabilistic prefixes.
 
-.. _maildir.synchronize_flags:
+.. nmconfig:: maildir.synchronize_flags
 
-maildir.synchronize\_flags
     If true, then the following maildir flags (in message filenames)
     will be synchronized with the corresponding notmuch tags:
 
@@ -205,9 +222,8 @@ maildir.synchronize\_flags
 
     Default: ``true``.
 
-.. _new.ignore:
+.. nmconfig:: new.ignore
 
-new.ignore
     A list to specify files and directories that will not be searched
     for messages by :any:`notmuch-new(1)`. Each entry in the list is either:
 
@@ -225,20 +241,21 @@ new.ignore
 
     Default: empty list.
 
-.. _new.tags:
+.. nmconfig:: new.tags
 
-new.tags
     A list of tags that will be added to all messages incorporated by
     **notmuch new**.
 
     Default: ``unread;inbox``.
 
-query.<name>
+.. nmconfig:: query.<name>
+
     Expansion for named query called <name>. See
     :any:`notmuch-search-terms(7)` for more information about named
     queries.
 
-search.exclude\_tags
+.. nmconfig:: search.exclude_tags
+
     A list of tags that will be excluded from search results by
     default. Using an excluded tag in a query will override that
     exclusion.
@@ -246,9 +263,7 @@ search.exclude\_tags
     Default: empty list. Note that :any:`notmuch-setup(1)` puts
     ``deleted;spam`` here when creating new configuration file.
 
-.. _show.extra_headers:
-
-show.extra\_headers
+.. nmconfig:: show.extra_headers
 
     By default :any:`notmuch-show(1)` includes the following headers
     in structured output if they are present in the message:
@@ -260,26 +275,28 @@ show.extra\_headers
 
     Default: empty list.
 
-squery.<name>
+.. nmconfig:: squery.<name>
+
     Expansion for named query called <name>, using s-expression syntax. See
     :any:`notmuch-sexp-queries(7)` for more information about s-expression
     queries.
 
-user.name
+.. nmconfig:: user.name
+
     Your full name.
 
     Default: ``$NAME`` variable if set, otherwise read from
     ``/etc/passwd``.
 
-user.other\_email
+.. nmconfig:: user.other_email
+
     A list of other email addresses at which you receive email
-    (see also, :ref:`user.primary_email <user.primary_email>`).
+    (see also, :nmconfig:`user.primary_email`)
 
     Default: not set.
 
-.. _user.primary_email:
+.. nmconfig:: user.primary_email
 
-user.primary\_email
     Your primary email address.
 
     Default: ``$EMAIL`` variable if set, otherwise constructed from
diff --git a/doc/man1/notmuch-git.rst b/doc/man1/notmuch-git.rst
new file mode 100644 (file)
index 0000000..ac1908b
--- /dev/null
@@ -0,0 +1,340 @@
+.. _notmuch-git(1):
+
+===========
+notmuch-git
+===========
+
+SYNOPSIS
+========
+
+**notmuch** **git** [-h] [-N] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand*
+
+**nmbug** [-h] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand*
+
+DESCRIPTION
+===========
+
+Manage notmuch tags with Git.
+
+OPTIONS
+-------
+
+Supported options for `notmuch git` include
+
+.. program:: notmuch-git
+
+.. option::  -h, --help
+
+   show help message and exit
+
+.. option:: -N, --nmbug
+
+   Set defaults for :option:`--tag-prefix` and :option:`--git-dir` suitable for the
+   :any:`notmuch` bug tracker
+
+.. option:: -C <repo>, --git-dir <repo>
+
+   Operate on git repository *repo*. See :ref:`repo_location` for
+   defaults.
+
+.. option:: -p <prefix>, --tag-prefix <prefix>
+
+   Operate only on tags with prefix *prefix*. See :ref:`prefix_val` for
+   defaults.
+
+.. option::   -v, --version
+
+   show notmuch-git's version number and exit
+
+.. option::   -l <level>, --log-level <level>
+
+   Log verbosity, one of: `critical`, `error`, `warning`, `info`,
+   `debug`. Defaults to `warning`.
+
+SUBCOMMANDS
+-----------
+
+For help on a particular subcommand, run: 'notmuch-git ... <command> --help'.
+
+.. program:: notmuch-git
+
+.. option:: archive [tree-ish] [arg ...]
+
+Dump a tar archive of a committed tag set using 'git archive'. See
+:any:`format` for details of the archive contents.
+
+   .. describe:: tree-ish
+
+   The tree or commit to produce an archive for. Defaults to 'HEAD'.
+
+   .. describe:: arg
+
+   If present, any optional arguments are passed through to
+   :manpage:`git-archive(1)`. Arguments to `git-archive` are reordered
+   so that *tree-ish* comes last.
+
+.. option:: checkout [-f|--force]
+
+Update the notmuch database from Git.
+
+This is mainly useful to discard your changes in notmuch relative
+to Git.
+
+   .. describe:: [-f|--force]
+
+   Override checks that prevent modifying tags for large fractions of
+   messages in the database. See also :nmconfig:`git.safe_fraction`.
+
+.. option:: clone <repository>
+
+Create a local `notmuch git` repository from a remote source.
+
+This wraps 'git clone', adding some options to avoid creating a
+working tree while preserving remote-tracking branches and
+upstreams.
+
+    .. describe:: repository
+
+    The (possibly remote) repository to clone from. See the URLS
+    section of :manpage:`git-clone(1)` for more information on
+    specifying repositories.
+
+.. option:: commit [-f|--force] [message]
+
+Commit prefix-matching tags from the notmuch database to Git.
+
+   .. describe:: message
+
+   Optional text for the commit message.
+
+   .. describe:: -f|--force
+
+   Override checks that prevent modifying tags for large fractions of
+   messages in the database. See also :nmconfig:`git.safe_fraction`.
+
+.. option:: fetch [remote]
+
+Fetch changes from the remote repository.
+
+    .. describe:: remote
+
+    Override the default configured in `branch.<name>.remote` to fetch
+    from a particular remote repository (e.g. `origin`).
+
+.. option:: help
+
+Show brief help for an `notmuch git` command.
+
+.. option:: init [--format-version=N]
+
+Create an empty `notmuch git` repository.
+
+This wraps 'git init' with a few extra steps to support subsequent
+status and commit commands.
+
+   .. describe:: --format-version=N
+
+   Create a repo in format version N. By default :any:`notmuch-git`
+   uses the highest supported version, which is the best choice for
+   most use-cases.
+
+.. option:: log [arg ...]
+
+A wrapper for 'git log'.
+
+   .. describe:: arg
+
+   Additional arguments are passed through to 'git log'.
+
+After running `notmuch git fetch`, you can inspect the changes with
+
+::
+
+   $ notmuch git log HEAD..@{upstream}
+
+.. option:: merge [reference]
+
+Merge changes from 'reference' into HEAD and load the result into notmuch.
+
+   .. describe:: reference
+
+   Reference, usually other branch heads, to merge into our
+   branch. Defaults to `@{upstream}`.
+
+.. option:: pull [repository] [refspec ...]
+
+Pull (merge) remote repository changes to notmuch.
+
+**pull** is equivalent to **fetch** followed by **merge**.  We use the
+Git-configured repository for your current branch
+(`branch.<name>.repository`, likely `origin`, and `branch.<name>.merge`,
+likely `master` or `main`).
+
+   .. describe:: repository
+
+   The "remote" repository that is the source of the pull. This parameter
+   can be either a URL (see the section GIT URLS in :manpage:`git-pull(1)`) or the
+   name of a remote (see the section REMOTES in :manpage:`git-pull(1)`).
+
+   .. describe:: refspec
+
+   Refspec (usually a branch name) to fetch and merge. See the
+   *refspec* entry in the OPTIONS section of :manpage:`git-pull(1`) for
+   other possibilities.
+
+.. option:: push [repository] [refspec]
+
+Push the local `notmuch git` Git state to a remote repository.
+
+    .. describe::  repository
+
+    The "remote" repository that is the destination of the push. This
+    parameter can be either a URL (see the section GIT URLS in
+    :manpage:`git-push(1)`) or the name of a remote (see the section
+    REMOTES in :manpage:`git-push(1)`).
+
+    .. describe:: refspec
+
+    Refspec (usually a branch name) to push. See the *refspec* entry in the OPTIONS section of
+    :manpage:`git-push(1)` for other possibilities.
+
+.. option:: status
+
+Show pending updates in notmuch or git repo.
+
+Prints lines of the form
+
+|  ng Message-Id tag
+
+where n is a single character representing notmuch database status
+
+   .. describe:: A
+
+   Tag is present in notmuch database, but not committed to nmbug
+   (equivalently, tag has been deleted in nmbug repo, e.g. by a
+   pull, but not restored to notmuch database).
+
+   .. describe:: D
+
+   Tag is present in nmbug repo, but not restored to notmuch
+   database (equivalently, tag has been deleted in notmuch).
+
+   .. describe:: U
+
+   Message is unknown (missing from local notmuch database).
+
+The second character *g* (if present) represents a difference between
+local and upstream branches. Typically `notmuch git fetch` needs to be
+run to update this.
+
+   .. describe:: a
+
+   Tag is present in upstream, but not in the local Git branch.
+
+   .. describe:: d
+
+   Tag is present in local Git branch, but not upstream.
+
+.. _format:
+
+REPOSITORY CONTENTS
+===================
+
+The tags are stored in the git repo (and exported) as a set of empty
+files. These empty files are contained within a directory named after
+the message-id.
+
+In what follows `encode()` represents a POSIX filesystem safe
+encoding. The encoding preserves alphanumerics, and the characters
+`+-_@=.,:`.  All other octets are replaced with `%` followed by a two
+digit hex number.
+
+Currently :any:`notmuch-git` can read any format version, but can only
+create (via :any:`init`) :ref:`version 1 <format_version_1>` repositories.
+
+.. _format_version_0:
+
+Version 0
+---------
+
+This is the legacy format created by the `nmbug` tool prior to release
+0.37.  For a message with Message-Id *id*, for each tag *tag*, there
+is an empty file with path
+
+       tags/ `encode` (*id*) / `encode` (*tag*)
+
+.. _format_version_1:
+
+Version 1
+---------
+
+In format version 1 and later, the format version is contained in a
+top level file called FORMAT.
+
+For a message with Message-Id *id*, for each tag *tag*, there
+is an empty file with path
+
+       tags/ `hash1` (*id*) / `hash2` (*id*) `encode` (*id*) / `encode` (*tag*)
+
+The hash functions each represent one byte of the `blake2b` hex
+digest.
+
+Compared to :ref:`version 0 <format_version_0>`, this reduces the
+number of subdirectories within each directory.
+
+.. _repo_location:
+
+REPOSITORY LOCATION
+===================
+
+:any:`notmuch-git` uses the first of the following with a non-empty
+value to locate the git repository.
+
+- Option :option:`--git-dir`.
+
+- Environment variable :envvar:`NOTMUCH_GIT_DIR`.
+
+- Configuration item :nmconfig:`git.path`
+
+- If invoked as `nmbug` or with the :option:`--nmbug` option,
+  :code:`$HOME/.nmbug`; otherwise
+  :code:`$XDG_DATA_HOME/notmuch/$NOTMUCH_PROFILE/git`.
+
+.. _prefix_val:
+
+PREFIX VALUE
+============
+
+:any:`notmuch-git` uses the first of the following with a non-null
+value to define the tag prefix.
+
+- Option :option:`--tag-prefix`.
+
+- Environment variable :envvar:`NOTMUCH_GIT_PREFIX`.
+
+- Configuration item :nmconfig:`git.tag_prefix`.
+
+- If invoked as `nmbug` or with the :option:`--nmbug` option,
+  :code:`notmuch::`, otherwise the empty string.
+
+ENVIRONMENT
+===========
+
+Variable :envvar:`NOTMUCH_PROFILE` influences :ref:`repo_location`.
+If it is unset, 'default' is assumed.
+
+.. envvar:: NOTMUCH_GIT_DIR
+
+   Default location of git repository. Overriden by :option:`--git-dir`.
+
+.. envvar:: NOTMUCH_GIT_PREFIX
+
+   Default tag prefix (filter). Overriden by :option:`--tag-prefix`.
+
+SEE ALSO
+========
+
+:any:`notmuch(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-tag(1)`
index fe2bf26b1395063ae9c90fbddd9c84d83a71c2ad..e05bd0b5730507bd1bed22ee30016713ed36d569 100644 (file)
@@ -14,12 +14,12 @@ DESCRIPTION
 
 **notmuch insert** reads a message from standard input and delivers it
 into the maildir directory given by configuration option
-:ref:`database.mail_root <database.mail_root>`, then incorporates the message into the notmuch
+:nmconfig:`database.mail_root`, then incorporates the message into the notmuch
 database. It is an alternative to using a separate tool to deliver the
 message then running :any:`notmuch-new(1)` afterwards.
 
 The new message will be tagged with the tags specified by the
-:ref:`new.tags <new.tags>` configuration option, then by operations specified on the
+:nmconfig:`new.tags` configuration option, then by operations specified on the
 command-line: tags prefixed by '+' are added while those prefixed by '-'
 are removed.
 
@@ -38,7 +38,7 @@ Supported options for **insert** include
 .. option:: --folder=<folder>
 
    Deliver the message to the specified folder, relative to the
-   top-level directory given by the value of **database.mail_root**. The
+   top-level directory given by the value of :nmconfig:`database.mail_root`. The
    default is the empty string, which means delivering to the
    top-level directory.
 
@@ -86,16 +86,15 @@ Supported options for **insert** include
    ``--decrypt=nostash`` without considering the security of your
    index.
 
-   See also :ref:`index.decrypt <index.decrypt>` in :any:`notmuch-config(1)`.
+   See also :nmconfig:`index.decrypt` in :any:`notmuch-config(1)`.
 
 CONFIGURATION
 =============
 
 Indexing is influenced by the configuration options
-:ref:`index.decrypt <index.decrypt>` and :ref:`index.header
-<index.header>`.  Tagging
-is controlled by :ref:`new.tags <new.tags>` and
-:ref:`maildir.synchronize_flags <maildir.synchronize_flags>`.  See
+:nmconfig:`index.decrypt` and :nmconfig:`index.header.\<prefix\>`.  Tagging
+is controlled by options :nmconfig:`new.tags` and
+:nmconfig:`maildir.synchronize_flags`.  See
 :any:`notmuch-config(1)` for details.
 
 EXIT STATUS
index 398f8813a834f464ea0b63eb6ce26df84fad6591..f0ed8eb836ffa6bfab11c77849947063692a6ce7 100644 (file)
@@ -82,10 +82,10 @@ CONFIGURATION
 =============
 
 Indexing is influenced by the configuration options
-:ref:`index.decrypt <index.decrypt>`, :ref:`index.header
-<index.header>`, and :ref:`new.ignore <new.ignore>`.  Tagging
-is controlled by :ref:`new.tags <new.tags>` and
-:ref:`maildir.synchronize_flags <maildir.synchronize_flags>`.  See
+:nmconfig:`index.decrypt`, :nmconfig:`index.header.\<prefix\>`
+and :nmconfig:`new.ignore`.  Tagging
+is controlled by :nmconfig:`new.tags` and
+:nmconfig:`maildir.synchronize_flags`.  See
 :any:`notmuch-config(1)` for details.
 
 EXIT STATUS
index fa0371f955e81fe49c15fefc5d7ccca5acd43261..4f39a9597e8c13e118e95b34901709e2861be1c7 100644 (file)
@@ -38,6 +38,12 @@ Supported options for **reply** include
 
 .. program:: reply
 
+.. option:: --duplicate=N
+
+   Reply to duplicate number N. The numbering starts from 1, and
+   matches the order used by :option:`show --duplicate` and
+   :option:`search --output=files <search --output>`.
+
 .. option:: --format=(default|json|sexp|headers-only)
 
    default
index 1b02d4079a34d4e9b5ba471eb2f90f9eadd458b4..2c0a0de6ad1679ccc2d2596acd3c8936a968e49c 100644 (file)
@@ -27,6 +27,12 @@ Supported options for **show** include
 
 .. program:: show
 
+.. option:: --duplicate=N
+
+   Output duplicate number N. The numbering starts from 1, and matches
+   the order used by :option:`search --duplicate` and
+   :option:`search --output=files <search --output>`
+
 .. option:: --entire-thread=(true|false)
 
    If true, **notmuch show** outputs all messages in the thread of
@@ -225,7 +231,7 @@ CONFIGURATION
 =============
 
 Structured output (json / sexp) is influenced by the configuration
-option :ref:`show.extra_headers <show.extra_headers>`. See
+option :nmconfig:`show.extra_headers`. See
 :any:`notmuch-config(1)` for details.
 
 EXIT STATUS
index c6ee30366264016846701ee4b3c4a9d4df343877..c488f12a9e7a58193486b5dd5fdf76fe31f62057 100644 (file)
@@ -130,14 +130,16 @@ and to restore from that dump.
 The :any:`notmuch-config(1)` command can be used to get or set
 settings in the notmuch configuration file.
 
-CUSTOM COMMANDS
----------------
+EXTERNAL COMMANDS
+-----------------
 
 If the given command is not known to notmuch, notmuch tries to execute
 the external **notmuch-<subcommand>** in :envvar:`PATH` instead. This
 allows users to have their own notmuch related tools to be run via the
 notmuch command. By design, this does not allow notmuch's own commands
-to be overridden using external commands.
+to be overridden using external commands.  The environment variable
+:envvar:`NOTMUCH_CONFIG` will be set according to :option:`--config`,
+if the latter is present.
 
 OPTION SYNTAX
 -------------
@@ -153,6 +155,23 @@ equivalent:
    notmuch --config:alt-config config get user.name
    notmuch --config alt-config config get user.name
 
+.. _duplicate-files:
+
+DUPLICATE MESSAGE FILES
+=======================
+
+Notmuch considers the :mailheader:`Message-ID` to be the primary
+identifier of message. Per :rfc:`5322` the :mailheader:`Message-ID` is
+supposed to be globally unique, but this fails in two distinct
+ways. When you receive copies of a message via a mechanism like
+:mailheader:`Cc` or via a mailing list, the copies are typically
+interchangeable. In the case of some broken mail sending software, the
+same :mailheader:`Message-ID` is used for completely unrelated
+messages. The options :option:`search --duplicate` and :option:`show
+--duplicate` options provide the user with control over which message
+file is displayed. Front ends will need to provide their own
+interface, see e.g. the Emacs front-end :any:`emacs-show-duplicates`.
+
 ENVIRONMENT
 ===========
 
index bc8e5086f342be9568cfc3baafac3e700c5c623b..d28f40bb3f3901dbecf8d8ceb37d81a538d020d2 100644 (file)
@@ -119,6 +119,12 @@ a message has one such attribute, and ``and`` otherwise.
 Term or phrase fields can contain arbitrarily complex queries made up
 from terms, operators, and modifiers, but not other fields.
 
+Range fields take one or two arguments specifying lower and upper
+bounds.  One argument is interpreted as identical upper and lower
+bounds. Either upper or lower bound may be specified as ``""`` or
+``*`` to specify the lowest possible lower bound or highest possible
+upper bound.
+
 .. _field-table:
 
 .. table:: Fields with supported modifiers
@@ -221,7 +227,7 @@ EXAMPLES
 
 ``(not Bob Marley)``
 
-    Match messages containing neither "Bob" nor "Marley", nor their stems,
+    Match messages containing neither "Bob" nor "Marley", nor their stems.
 
 ``"quick fox"`` ``quick-fox`` ``quick@fox``
 
@@ -230,16 +236,28 @@ EXAMPLES
 
 ``(folder (of (id 1234@invalid)))``
 
-    Match any message in the same folder as the one with Message-Id "1234@invalid"
+    Match any message in the same folder as the one with Message-Id "1234\@invalid".
 
 ``(id 1234@invalid blah@test)``
 
-    Matches Message-Id "1234@invalid" *or* Message-Id "blah@test"
+    Matches Message-Id "1234\@invalid" *or* Message-Id "blah\@test".
 
 ``(and (infix "date:2009-11-18..2009-11-18") (tag unread))``
 
     Match messages in the given date range with tag unread.
 
+``(and (date 2009-11-18 2009-11-18) (tag unread))``
+
+    Match messages in the given date range with tag unread.
+
+``(and (date 2009-11-18 *) (tag unread))``
+
+    Match messages from 2009-11-18 or later with tag unread.
+
+``(and (date * 2009-11-18) (tag unread))``
+
+    Match messages from 2009-11-18 or earlier with tag unread.
+
 ``(starts-with prelim)``
 
     Match any words starting with "prelim".
@@ -260,18 +278,18 @@ EXAMPLES
 
 ``(thread (of (id 1234@invalid)))``
 
-    Match any message in the same thread as the one with Message-Id "1234@invalid"
+    Match any message in the same thread as the one with Message-Id "1234\@invalid".
 
 ``(thread (matching (from bob@example.com) (to bob@example.com)))``
 
     Match any (messages in) a thread containing a message from
-    "bob@example.com" and a (possibly distinct) message to "bob at
-    example.com")
+    "bob\@example.com" and a (possibly distinct) message to
+    "bob\@example.com".
 
 ``(to (or bob@example.com mallory@example.org))`` ``(or (to bob@example.com) (to mallory@example.org))``
 
-    Match in the "To" or "Cc" headers, "bob@example.com",
-    "mallory@example.org", and also "bob@example.com.au" since it
+    Match in the "To" or "Cc" headers, "bob\@example.com",
+    "mallory\@example.org", and also "bob\@example.com.au" since it
     contains the adjacent triple "bob", "example", "com".
 
 ``(not (to *))``
@@ -281,7 +299,7 @@ EXAMPLES
 ``(List *)``
 
     Match messages with a non-empty List-Id header, assuming
-    configuration ``index.header.List=List-Id``
+    configuration ``index.header.List=List-Id``.
 
 .. _macro_examples:
 
index 41f62390b79c8dee4590eadba30f2ffca5dc32ea..846f5e671cf292b3bf3ddeedde94acd7b99da94f 100644 (file)
@@ -1,6 +1,8 @@
-=============
-notmuch-emacs
-=============
+.. _notmuch-emacs:
+
+==========================
+Emacs Frontend for Notmuch
+==========================
 
 About this Manual
 =================
@@ -46,39 +48,95 @@ a mouse or by positioning the cursor and pressing ``<return>``
 |                    Customize Notmuch or this page.
 
 You can change the overall appearance of the notmuch-hello screen by
-customizing the variable :index:`notmuch-hello-sections`.
+customizing the variables
+
+.. el:defcustom:: notmuch-hello-sections
+
+       |docstring::notmuch-hello-sections|
+
+.. el:defcustom:: notmuch-hello-thousands-separator
+
+       |docstring::notmuch-hello-thousands-separator|
+
+.. el:defcustom:: notmuch-show-logo
+
+       |docstring::notmuch-show-logo|
+
+.. el:defcustom:: notmuch-column-control
+
+    Controls the number of columns for saved searches/tags in notmuch view.
+
+    This variable has three potential types of values:
+
+    .. describe:: t
+
+       Automatically calculate the number of columns possible based
+       on the tags to be shown and the window width.
+
+    .. describe:: integer <n>
+
+       A lower bound on the number of characters that will
+       be used to display each column.
+
+    .. describe:: float <f>
 
+       A fraction of the window width that is the lower bound on the
+       number of characters that should be used for each column.
 
+    So:
+
+    - if you would like two columns of tags, set this to 0.5.
+
+    - if you would like a single column of tags, set this to 1.0.
+
+    - if you would like tags to be 30 characters wide, set this to 30.
+
+    - if you don't want to worry about all of this nonsense, leave
+      this set to `t`.
+
+.. el:defcustom:: notmuch-show-empty-saved-searches
+
+   |docstring::notmuch-show-empty-saved-searches|
 
 notmuch-hello key bindings
 --------------------------
 
-``<tab>``
+.. el:define-key:: <tab>
+
     Move to the next widget (button or text entry field)
 
-``<backtab>``
+.. el:define-key:: <backtab>
+
     Move to the previous widget.
 
-``<return>``
+.. el:define-key:: <return>
+
     Activate the current widget.
 
-``g`` ``=``
+.. el:define-key:: g
+                   =
+
     Refresh the buffer; mainly update the counts of messages for various
     saved searches.
 
-``G``
+.. el:define-key:: G
+
     Import mail, See :ref:`importing`
 
-``m``
+.. el:define-key:: m
+
     Compose a message
 
-``s``
+.. el:define-key:: s
+
     Search the notmuch database using :ref:`notmuch-search`
 
-``v``
+.. el:define-key:: v
+
     Print notmuch version
 
-``q``
+.. el:define-key:: q
+
     Quit
 
 .. _saved-searches:
@@ -96,25 +154,28 @@ The saved searches default to various common searches such as
 ``tag:inbox`` to access the inbox and ``tag:unread`` to access all
 unread mail, but there are several options for customization:
 
-:index:`notmuch-saved-searches`
+.. el:defcustom:: notmuch-saved-searches
+
     The list of saved searches, including names, queries, and
     additional per-query options.
 
-:index:`notmuch-saved-search-sort-function`
+.. el:defcustom:: notmuch-saved-search-sort-function
+
     This variable controls how saved searches should be sorted. A value
     of ``nil`` displays the saved searches in the order they are stored
     in ‘notmuch-saved-searches’.
 
-:index:`notmuch-column-control`
-    Controls the number of columns for displaying saved-searches/tags
-
 Search Box
 ----------
 
 The search box lets the user enter a Notmuch query. See section
 “Description” in Notmuch Query Syntax, for more info on Notmuch query
 syntax. A history of recent searches is also displayed by default. The
-latter is controlled by the variable :index:`notmuch-hello-recent-searches-max`.
+latter is controlled by the variable `notmuch-hello-recent-searches-max`.
+
+.. el:defcustom:: notmuch-hello-recent-searches-max
+
+              |docstring::notmuch-hello-recent-searches-max|
 
 Known Tags
 ----------
@@ -123,15 +184,14 @@ One special kind of saved search provided by default is for each
 individual tag defined in the database. This can be controlled via the
 following variables.
 
-:index:`notmuch-hello-tag-list-make-query`
+.. el:defcustom:: notmuch-hello-tag-list-make-query
+
     Control how to construct a search (“virtual folder”) from a given
     tag.
 
-:index:`notmuch-hello-hide-tags`
-    Which tags not to display at all.
+.. el:defcustom:: notmuch-hello-hide-tags
 
-:index:`notmuch-column-control`
-    Controls the number of columns for displaying saved-searches/tags
+    Which tags not to display at all.
 
 .. _notmuch-search:
 
@@ -150,38 +210,86 @@ The main purpose of the ``notmuch-search-mode`` buffer is to act as a
 menu of results that the user can explore further by pressing
 ``<return>`` on the appropriate line.
 
-``n,C-n,<down>``
+.. el:define-key:: n
+   C-n
+   <down>
+
     Move to next line
 
-``p,C-p,<up>``
+.. el:define-key::
+   p
+   C-p
+   <up>
+
     Move to previous line
 
-``<return>``
+.. el:define-key:: <return>
+
     Open thread on current line in :ref:`notmuch-show` mode
 
-``g`` ``=``
+.. el:define-key:: g
+   =
+
     Refresh the buffer
 
-``?``
+.. el:define-key:: ?
+
     Display full set of key bindings
 
 The presentation of results can be controlled by the following
 variables.
 
-:index:`notmuch-search-result-format`
-    Control how each thread of messages is presented in the
-    ``notmuch-show-mode`` buffer
+.. el:defcustom:: notmuch-search-result-format
+
+   |docstring::notmuch-search-result-format|
+
+   If the car of an element in notmuch-search-result-format is a
+   function, insert the result of calling the function into the buffer.
+
+   This allows a user to generate custom fields in the output of a
+   search result. For example, with the following settings, the first
+   few characters on each line of the search result are used to show
+   information about some significant tags associated with the thread.
+
+   .. code:: lisp
+
+      (defun -notmuch-result-flags (format-string result)
+        (let ((tags-to-letters '(("flagged" . "!")
+                                 ("unread" . "u")
+                                 ("mine" . "m")
+                                 ("sent" . "s")
+                                 ("replied" . "r")))
+              (tags (plist-get result :tags)))
+          (format format-string
+                  (mapconcat (lambda (t2l)
+                               (if (member (car t2l) tags)
+                                   (cdr t2l)
+                                 " "))
+                             tags-to-letters ""))))
+
+      (setq notmuch-search-result-format '((-notmuch-result-flags . "%s ")
+                                           ("date" . "%12s ")
+                                           ("count" . "%9s ")
+                                           ("authors" . "%-30s ")
+                                           ("subject" . "%s ")
+                                           ("tags" . "(%s)")))
+
+   See also :el:defcustom:`notmuch-tree-result-format` and
+   :el:defcustom:`notmuch-unthreaded-result-format`.
+
+.. el:defcustom:: notmuch-search-oldest-first
 
-:index:`notmuch-search-oldest-first`
     Display the oldest threads at the top of the buffer
 
 It is also possible to customize how the name of buffers containing
 search results is formatted using the following variables:
 
-:index:`notmuch-search-buffer-name-format`
+.. el:defcustom:: notmuch-search-buffer-name-format
+
        |docstring::notmuch-search-buffer-name-format|
 
-:index:`notmuch-saved-search-buffer-name-format`
+.. el:defcustom:: notmuch-saved-search-buffer-name-format
+
        |docstring::notmuch-saved-search-buffer-name-format|
 
 
@@ -198,43 +306,113 @@ signatures, already-read messages), are hidden. You can make
 these parts visible by clicking with the mouse button or by
 pressing RET after positioning the cursor on a hidden part.
 
-``<space>``
+.. el:define-key:: <space>
+
     Scroll the current message (if necessary),
     advance to the next message, or advance to the next thread (if
     already on the last message of a thread).
 
-``c``
+.. el:define-key:: c
+
     :ref:`show-copy`
 
-``N``
+.. el:define-key:: N
+
     Move to next message
 
-``P``
+.. el:define-key:: P
+
     Move to previous message (or start of current message)
 
-``n``
+.. el:define-key:: n
+
     Move to next matching message
 
-``p``
+.. el:define-key:: p
+
     Move to previous matching message
 
-``+,-``
+.. el:define-key:: +
+                   -
+
     Add or remove arbitrary tags from the current message.
 
-``?``
+.. el:define-key:: !
+
+    |docstring::notmuch-show-toggle-elide-non-matching|
+
+.. el:define-key:: ?
+
     Display full set of key bindings
 
-Display of messages can be controlled by the following variables
+Display of messages can be controlled by the following variables; see also :ref:`show-large`.
+
+.. el:defcustom:: notmuch-message-headers
 
-:index:`notmuch-message-headers`
        |docstring::notmuch-message-headers|
 
-:index:`notmuch-message-headers-visible`
+.. el:defcustom:: notmuch-message-headers-visible
+
        |docstring::notmuch-message-headers-visible|
 
-:index:`notmuch-show-header-line`
+.. el:defcustom:: notmuch-show-header-line
+
        |docstring::notmuch-show-header-line|
 
+.. el:defcustom:: notmuch-multipart/alternative-discouraged
+
+   Which mime types to hide by default for multipart messages.
+
+   Can either be a list of mime types (as strings) or a function
+   mapping a plist representing the current message to such a list.
+   The following example function would discourage `text/html` and
+   `multipart/related` generally, but discourage `text/plain` should
+   the message be sent from `whatever@example.com`.
+
+   .. code:: lisp
+
+      (defun my--determine-discouraged (msg)
+        (let* ((headers (plist-get msg :headers))
+               (from (or (plist-get headers :From) "")))
+          (cond
+           ((string-match "whatever@example.com" from)
+            (list "text/plain"))
+           (t
+            (list "text/html" "multipart/related")))))
+
+.. _show-large:
+
+Dealing with large messages and threads
+---------------------------------------
+
+If you are finding :ref:`notmuch-show` is annoyingly slow displaying
+large messages, you can customize
+:el:defcustom:`notmuch-show-max-text-part-size`.  If you want to speed up the
+display of large threads (with or without large messages), there are
+several options.  First, you can display the same query in one of the
+other modes. :ref:`notmuch-unthreaded` is the most robust for
+extremely large queries, but :ref:`notmuch-tree` is also be faster
+than :ref:`notmuch-show` in general, since it only renders a single
+message a time. If you prefer to stay with the rendered thread
+("conversation") view of :ref:`notmuch-show`, you can customize the
+variables :el:defcustom:`notmuch-show-depth-limit`,
+:el:defcustom:`notmuch-show-height-limit` and
+:el:defcustom:`notmuch-show-max-text-part-size` to limit the amount of
+rendering done initially. Note that these limits are implicitly
+*OR*-ed together, and combinations might have surprising effects.
+
+.. el:defcustom:: notmuch-show-depth-limit
+
+       |docstring::notmuch-show-depth-limit|
+
+.. el:defcustom:: notmuch-show-height-limit
+
+       |docstring::notmuch-show-height-limit|
+
+.. el:defcustom:: notmuch-show-max-text-part-size
+
+       |docstring::notmuch-show-max-text-part-size|
+
 .. _show-copy:
 
 Copy to kill-ring
@@ -245,44 +423,92 @@ but notmuch also provides some shortcuts. These keys are available in
 :ref:`notmuch-show`, and :ref:`notmuch-tree`. A subset are available
 in :ref:`notmuch-search`.
 
-``c F``        ``notmuch-show-stash-filename``
+.. el:define-key:: c F
+   M-x notmuch-show-stash-filename
+
    |docstring::notmuch-show-stash-filename|
 
-``c G`` ``notmuch-show-stash-git-send-email``
+.. el:define-key:: c G
+   M-x notmuch-show-stash-git-send-email
+
    |docstring::notmuch-show-stash-git-send-email|
 
-``c I`` ``notmuch-show-stash-message-id-stripped``
+.. el:define-key:: c I
+   M-x notmuch-show-stash-message-id-stripped
+
    |docstring::notmuch-show-stash-message-id-stripped|
 
-``c L`` ``notmuch-show-stash-mlarchive-link-and-go``
+.. el:define-key:: c L
+   M-x notmuch-show-stash-mlarchive-link-and-go
+
    |docstring::notmuch-show-stash-mlarchive-link-and-go|
 
-``c T`` ``notmuch-show-stash-tags``
+.. el:define-key:: c T
+   M-x notmuch-show-stash-tags
+
    |docstring::notmuch-show-stash-tags|
 
-``c c`` ``notmuch-show-stash-cc``
+.. el:define-key:: c c
+   M-x notmuch-show-stash-cc
+
    |docstring::notmuch-show-stash-cc|
 
-``c d`` ``notmuch-show-stash-date``
+.. el:define-key:: c d
+   M-x notmuch-show-stash-date
+
    |docstring::notmuch-show-stash-date|
 
-``c f`` ``notmuch-show-stash-from``
+.. el:define-key:: c f
+   M-x notmuch-show-stash-from
+
    |docstring::notmuch-show-stash-from|
 
-``c i`` ``notmuch-show-stash-message-id``
+.. el:define-key:: c i
+   M-x notmuch-show-stash-message-id
+
    |docstring::notmuch-show-stash-message-id|
 
-``c l`` ``notmuch-show-stash-mlarchive-link``
+.. el:define-key:: c l
+   M-x notmuch-show-stash-mlarchive-link
+
    |docstring::notmuch-show-stash-mlarchive-link|
 
-``c s`` ``notmuch-show-stash-subject``
+.. el:define-key:: c s
+   M-x notmuch-show-stash-subject
+
    |docstring::notmuch-show-stash-subject|
 
-``c t`` ``notmuch-show-stash-to``
+.. el:define-key:: c t
+   M-x notmuch-show-stash-to
+
    |docstring::notmuch-show-stash-to|
 
-``c ?``
-    Show all available copying commands
+.. el:define-key:: c ?
+   M-x notmuch-subkeymap-help
+
+   Show all available copying commands
+
+.. _emacs-show-duplicates:
+
+Dealing with duplicates
+-----------------------
+
+If there are are multiple files with the same :mailheader:`Message-ID`
+(see :any:`duplicate-files`), then :any:`notmuch-show` displays the
+number of duplicates and identifies the current duplicate. In the
+following example duplicate 3 of 5 is displayed.
+
+.. code-block::
+   :emphasize-lines: 1
+
+    M. Mustermann <max@example.com> (Sat, 30 Jul 2022 10:33:10 -0300) (inbox signed)      3/5
+    Subject: Re: Multiple files per message in emacs
+    To: notmuch@notmuchmail.org
+
+.. el:define-key:: %
+   M-x notmuch-show-choose-duplicate
+
+   |docstring::notmuch-show-choose-duplicate|
 
 .. _notmuch-tree:
 
@@ -294,43 +520,93 @@ email archives. Each line in the buffer represents a single
 message giving the relative date, the author, subject, and any
 tags.
 
-``c``
+.. el:define-key:: c
+
     :ref:`show-copy`
 
-``<return>``
+.. el:define-key:: <return>
+
    Displays that message.
 
-``N``
+.. el:define-key:: N
+
     Move to next message
 
-``P``
+.. el:define-key:: P
+
     Move to previous message
 
-``n``
+.. el:define-key:: n
+
     Move to next matching message
 
-``p``
+.. el:define-key:: p
+
     Move to previous matching message
 
-``o`` ``notmuch-tree-toggle-order``
+.. el:define-key:: o
+   M-x notmuch-tree-toggle-order
+
    |docstring::notmuch-tree-toggle-order|
 
-``l`` ``notmuch-tree-filter``
+.. el:define-key:: l
+   M-x notmuch-tree-filter
+
    Filter or LIMIT the current search results based on an additional query string
 
-``t`` ``notmuch-tree-filter-by-tag``
+.. el:define-key:: t
+   M-x notmuch-tree-filter-by-tag
+
    Filter the current search results based on an additional tag
 
 
-``g`` ``=``
+.. el:define-key:: g
+   =
+
     Refresh the buffer
 
-``?``
+.. el:define-key:: ?
+
     Display full set of key bindings
 
 As is the case with :ref:`notmuch-search`, the presentation of results
 can be controlled by the variable ``notmuch-search-oldest-first``.
 
+.. el:defcustom:: notmuch-tree-result-format
+
+   |docstring::notmuch-tree-result-format|
+
+   The following example shows how to optionally display recipients instead
+   of authors for sent mail (assuming the user is named Mustermann).
+
+   .. code:: lisp
+
+      (defun -notmuch-authors-or-to (format-string result)
+        (let* ((headers (plist-get result :headers))
+               (to (plist-get headers :To))
+               (author (plist-get headers :From))
+               (face (if (plist-get result :match)
+                         'notmuch-tree-match-author-face
+                       'notmuch-tree-no-match-author-face)))
+          (propertize
+           (format format-string
+                   (if (string-match "Mustermann" author)
+                       (concat "To:" (notmuch-tree-clean-address to))
+                     author))
+           'face face)))
+
+      (setq notmuch-tree-result-format
+            '(("date" . "%12s  ")
+              (-notmuch-authors-or-to . "%-20.20s")
+              ((("tree" . "%s")
+                ("subject" . "%s"))
+               . " %-54s ")
+              ("tags" . "(%s)")))
+
+   See also :el:defcustom:`notmuch-search-result-format` and
+   :el:defcustom:`notmuch-unthreaded-result-format`.
+
+
 .. _notmuch-unthreaded:
 
 notmuch-unthreaded
@@ -342,19 +618,32 @@ is presented.
 
 Keybindings are the same as :any:`notmuch-tree`.
 
+.. el:defcustom:: notmuch-unthreaded-result-format
+
+   |docstring::notmuch-unthreaded-result-format|
+
+   See also :el:defcustom:`notmuch-search-result-format` and
+   :el:defcustom:`notmuch-tree-result-format`.
+
 Global key bindings
 ===================
 
 Several features are accessible from most places in notmuch through the
 following key bindings:
 
-``j``
+.. el:define-key:: j
+
     Jump to saved searches using :ref:`notmuch-jump`.
 
-``k``
+.. el:define-key:: k
+
     Tagging operations using :ref:`notmuch-tag-jump`
 
-``C-_`` ``C-/`` ``C-x u``: Undo previous tagging operation using :ref:`notmuch-tag-undo`
+.. el:define-key:: C-_
+   C-/
+   C-x u
+
+   Undo previous tagging operation using :any:`notmuch-tag-undo`
 
 .. _notmuch-jump:
 
@@ -381,11 +670,10 @@ prefix (:kbd:`C-u k`), notmuch displays a menu of the reverses of the
 operations specified in ``notmuch-tagging-keys``; i.e. each
 ``+tag`` is replaced by ``-tag`` and vice versa.
 
-:index:`notmuch-tagging-keys`
+.. el:defcustom:: notmuch-tagging-keys
 
   |docstring::notmuch-tagging-keys|
 
-.. _notmuch-tag-undo:
 
 notmuch-tag-undo
 ----------------
@@ -393,17 +681,21 @@ notmuch-tag-undo
 Each notmuch buffer supporting tagging operations (i.e buffers in
 :any:`notmuch-show`, :any:`notmuch-search`, :any:`notmuch-tree`, and
 :any:`notmuch-unthreaded` mode) keeps a local stack of tagging
-operations. These can be undone via ``notmuch-tag-undo``. By default
+operations. These can be undone via :any:`notmuch-tag-undo`. By default
 this is bound to the usual Emacs keys for undo.
 
-:index:`notmuch-tag-undo`
+.. el:define-key::  C-_
+   C-/
+   C-x u
+   M-x notmuch-tag-undo
 
    |docstring::notmuch-tag-undo|
 
 Buffer navigation
 =================
 
-:index:`notmuch-cycle-notmuch-buffers`
+.. el:define-key:: M-x notmuch-cycle-notmuch-buffers
+
    |docstring::notmuch-cycle-notmuch-buffers|
 
 Configuration
@@ -414,22 +706,33 @@ Configuration
 Importing Mail
 --------------
 
-:index:`notmuch-poll`
+.. el:define-key:: M-x notmuch-poll
+
    |docstring::notmuch-poll|
 
-:index:`notmuch-poll-script`
+.. el:defcustom:: notmuch-poll-script
+
    |docstring::notmuch-poll-script|
 
 Sending Mail
 ------------
 
-:index:`mail-user-agent`
+.. el:defcustom:: mail-user-agent
 
        Emacs consults the variable :code:`mail-user-agent` to choose a mail
        sending package for commands like :code:`report-emacs-bug` and
        :code:`compose-mail`.  To use ``notmuch`` for this, customize this
        variable to the symbol :code:`notmuch-user-agent`.
 
+.. el:defcustom:: message-dont-reply-to-names
+
+       When composing mail replies, Emacs's message mode uses the
+       variable :code:`message-dont-reply-to-names` to exclude
+       recipients matching a given collection of regular expressions
+       or satisfying an arbitrary predicate.  Notmuch's MUA inherits
+       this standard mechanism and will honour your customization of
+       this variable.
+
 Init File
 ---------
 
diff --git a/doc/queries.rst b/doc/queries.rst
new file mode 100644 (file)
index 0000000..b76e71e
--- /dev/null
@@ -0,0 +1,9 @@
+Notmuch Queries
+===============
+
+.. toctree::
+   :titlesonly:
+
+   man7/notmuch-search-terms
+   man7/notmuch-sexp-queries
+   man7/notmuch-properties
index 6fc71cc737995a5f4a66d205a0be58e458281ab3..84ba8c5e56de2aa151682bea74e30a4f3808b2e3 100644 (file)
@@ -567,12 +567,20 @@ Take wildcards into account."
           (string= (downcase t1)
                    (downcase t2))))))
 
-(defvar notmuch-multipart/alternative-discouraged
+(defcustom notmuch-multipart/alternative-discouraged
   '(;; Avoid HTML parts.
     "text/html"
     ;; multipart/related usually contain a text/html part and some
     ;; associated graphics.
-    "multipart/related"))
+    "multipart/related")
+  "Which mime types to hide by default for multipart messages.
+
+Can either be a list of mime types (as strings) or a function
+mapping a plist representing the current message to such a list.
+See Info node `(notmuch-emacs) notmuch-show' for a sample function."
+  :group 'notmuch-show
+  :type '(radio (repeat :tag "MIME Types" string)
+               (function :tag "Function")))
 
 (defun notmuch-multipart/alternative-determine-discouraged (msg)
   "Return the discouraged alternatives for the specified message."
@@ -1021,6 +1029,20 @@ status."
 
 (defvar-local notmuch-show-process-crypto nil)
 
+(defun notmuch--run-show (search-terms &optional duplicate)
+  "Return a list of threads of messages matching SEARCH-TERMS.
+
+A thread is a forest or list of trees. A tree is a two element
+list where the first element is a message, and the second element
+is a possibly empty forest of replies."
+  (let ((args '("show" "--format=sexp" "--format-version=5")))
+    (when notmuch-show-process-crypto
+      (setq args (append args '("--decrypt=true"))))
+    (when duplicate
+      (setq args (append args (list (format "--duplicate=%d" duplicate)))))
+    (setq args (append args search-terms))
+    (apply #'notmuch-call-notmuch-sexp args)))
+
 ;;; Generic Utilities
 
 (defun notmuch-interactive-region ()
@@ -1037,6 +1059,14 @@ region if the region is active, or both `point' otherwise."
   'notmuch-interactive-region
   "notmuch 0.29")
 
+(defun notmuch--inline-override-types ()
+  "Override mm-inline-override-types to stop application/*
+parts from being displayed unless the user has customized
+it themselves."
+  (if (equal mm-inline-override-types
+            (eval (car (get 'mm-inline-override-types 'standard-value))))
+      (cons "application/.*" mm-inline-override-types)
+    mm-inline-override-types))
 ;;; _
 
 (provide 'notmuch-lib)
index c679373b175955092858c7bfe8366a4aa088c91e..ac878a619d07e7c4947f1c359b9d6825580516b6 100644 (file)
 
 ;;; Code:
 
+(eval-when-compile (require 'subr-x))
+
 (require 'message)
+(require 'gmm-utils)
 (require 'mm-view)
 (require 'format-spec)
 
@@ -234,11 +237,12 @@ Typically this is added to `notmuch-mua-send-hook'."
 
 ;;; Mua reply
 
-(defun notmuch-mua-reply (query-string &optional sender reply-all)
-  (let ((args '("reply" "--format=sexp" "--format-version=5"))
-       (process-crypto notmuch-show-process-crypto)
-       reply
-       original)
+(defun notmuch-mua-reply (query-string &optional sender reply-all duplicate)
+  (let* ((duparg (and duplicate (list (format "--duplicate=%d" duplicate))))
+        (args `("reply" "--format=sexp" "--format-version=5" ,@duparg))
+        (process-crypto notmuch-show-process-crypto)
+        reply
+        original)
     (when process-crypto
       (setq args (append args '("--decrypt=true"))))
     (if reply-all
@@ -316,7 +320,9 @@ Typically this is added to `notmuch-mua-send-hook'."
                ;; text.
                (notmuch-show-process-crypto process-crypto)
                ;; Don't indent multipart sub-parts.
-               (notmuch-show-indent-multipart nil))
+               (notmuch-show-indent-multipart nil)
+               ;; Stop certain mime types from being inlined
+               (mm-inline-override-types (notmuch--inline-override-types)))
             ;; We don't want sigstatus buttons (an information leak and usually wrong anyway).
             (cl-letf (((symbol-function 'notmuch-crypto-insert-sigstatus-button) #'ignore)
                       ((symbol-function 'notmuch-crypto-insert-encstatus-button) #'ignore))
@@ -380,10 +386,31 @@ instead of `message-mode' and SWITCH-FUNCTION is mandatory."
     (erase-buffer)
     (notmuch-message-mode)))
 
+(defun notmuch-mua--remove-dont-reply-to-names ()
+  (when-let* ((nr (if (functionp message-dont-reply-to-names)
+                     message-dont-reply-to-names
+                   (gmm-regexp-concat message-dont-reply-to-names)))
+             (nr-filter
+              (if (functionp nr)
+                  (lambda (mail) (and (not (funcall nr mail)) mail))
+                (lambda (mail) (and (not (string-match-p nr mail)) mail)))))
+    (dolist (header '("To" "Cc"))
+      (when-let ((v (message-fetch-field header)))
+       (let* ((tokens (mapcar #'string-trim (message-tokenize-header v)))
+              (good-tokens (delq nil (mapcar nr-filter tokens)))
+              (addr (and good-tokens (mapconcat #'identity good-tokens ", "))))
+         (message-replace-header header addr))))))
+
 (defun notmuch-mua-mail (&optional to subject other-headers _continue
                                   switch-function yank-action send-actions
                                   return-action &rest ignored)
-  "Invoke the notmuch mail composition window."
+  "Invoke the notmuch mail composition window.
+
+The position of point when the function returns differs depending
+on the values of TO and SUBJECT.  If both are non-nil, point is
+moved to the message's body.  If SUBJECT is nil but TO isn't,
+point is moved to the \"Subject:\" header.  Otherwise, point is
+moved to the \"To:\" header."
   (interactive)
   (when notmuch-mua-user-agent-function
     (let ((user-agent (funcall notmuch-mua-user-agent-function)))
@@ -414,11 +441,15 @@ instead of `message-mode' and SWITCH-FUNCTION is mandatory."
        (message-this-is-mail t))
     (message-setup-1 headers yank-action send-actions return-action))
   (notmuch-fcc-header-setup)
+  (notmuch-mua--remove-dont-reply-to-names)
   (message-sort-headers)
   (message-hide-headers)
   (set-buffer-modified-p nil)
   (notmuch-mua-maybe-set-window-dedicated)
-  (message-goto-to))
+  (cond
+   ((and to subject) (message-goto-body))
+   (to (message-goto-subject))
+   (t (message-goto-to))))
 
 (defvar notmuch-mua-sender-history nil)
 
@@ -510,12 +541,13 @@ the From: address."
       (message-hide-headers)
       (set-buffer-modified-p nil))))
 
-(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all)
+(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all duplicate)
   "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."
+will be addressed to all recipients of the source message.  If
+DUPLICATE is non-nil, based the reply on that duplicate file"
   ;; `select-active-regions' is 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
@@ -530,7 +562,7 @@ will be addressed to all recipients of the source message."
   (let ((sender (and prompt-for-sender
                     (notmuch-mua-prompt-for-sender)))
        (select-active-regions nil))
-    (notmuch-mua-reply query-string sender reply-all)
+    (notmuch-mua-reply query-string sender reply-all duplicate)
     (deactivate-mark)))
 
 ;;; Checks
index 5c7f4f8d8918504a70e23c436c6f644c44f1f2ce..2a46144c49846dcf3301ac5b365951fcab731239 100644 (file)
 
 ;;; Basic query function
 
-(defun notmuch-query-get-threads (search-terms)
-  "Return a list of threads of messages matching SEARCH-TERMS.
-
-A thread is a forest or list of trees. A tree is a two element
-list where the first element is a message, and the second element
-is a possibly empty forest of replies."
-  (let ((args '("show" "--format=sexp" "--format-version=5")))
-    (when notmuch-show-process-crypto
-      (setq args (append args '("--decrypt=true"))))
-    (setq args (append args search-terms))
-    (apply #'notmuch-call-notmuch-sexp args)))
+(define-obsolete-function-alias
+  'notmuch-query-get-threads
+  #'notmuch--run-show
+  "notmuch 0.37")
 
 ;;; Mapping functions across collections of messages
 
@@ -60,7 +53,7 @@ Flatten results to a list.  See the function
 (defun notmuch-query-map-tree (fn tree)
   "Apply function FN to every message in TREE.
 Flatten results to a list.  See the function
-`notmuch-query-get-threads' for more information."
+`notmuch--run-show' for more information."
   (cons (funcall fn (car tree))
        (notmuch-query-map-forest fn (cadr tree))))
 
@@ -70,7 +63,11 @@ Flatten results to a list.  See the function
   "Return a list of message-ids of messages that match SEARCH-TERMS."
   (notmuch-query-map-threads
    (lambda (msg) (plist-get msg :id))
-   (notmuch-query-get-threads search-terms)))
+   (notmuch--run-show search-terms)))
+
+;;; Everything in this library is obsolete
+(dolist (fun '(map-aux map-threads map-forest map-tree get-message-ids))
+  (make-obsolete (intern (format "notmuch-query-%s" fun)) nil "notmuch 0.37"))
 
 (provide 'notmuch-query)
 
index 7c1f02c92f1499be58df08929863d4e1aa013118..2dcef98167c9e0e1162d515bdf8829de687099f0 100644 (file)
@@ -32,7 +32,6 @@
 
 (require 'notmuch-lib)
 (require 'notmuch-tag)
-(require 'notmuch-query)
 (require 'notmuch-wash)
 (require 'notmuch-mua)
 (require 'notmuch-crypto)
@@ -85,8 +84,56 @@ visible for any given message."
   :group 'notmuch-show)
 
 (defcustom notmuch-show-header-line t
-  "Show a header line with the current message's subject."
-  :type 'boolean
+  "Show a header line in notmuch show buffers.
+
+If t (the default), the header line will contain the current
+message's subject.
+
+If a string, this value is interpreted as a format string to be
+passed to `format-spec` with `%s` as the substitution variable
+for the message's subject.  E.g., to display the subject trimmed
+to a maximum of 80 columns, you could use \"%>-80s\" as format.
+
+If you assign to this variable a function, it will be called with
+the subject as argument, and the return value will be used as the
+header line format.  Since the function is called with the
+message buffer as the current buffer, it is also possible to
+access any other properties of the message, using for instance
+notmuch-show functions such as
+`notmuch-show-get-message-properties'.
+
+Finally, if this variable is set to nil, no header is
+displayed."
+  :type '(choice (const :tag "No header" ni)
+                 (const :tag "Subject" t)
+                 (string :tag "Format")
+                (function :tag "Function"))
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-depth-limit nil
+  "Depth beyond which message bodies are displayed lazily.
+
+If bound to an integer, any message with tree depth greater than
+this will have its body display lazily, initially
+inserting only a button.
+
+If this variable is set to nil (the default) no such lazy
+insertion is done."
+  :type '(choice (const :tag "No limit" nil)
+                 (number :tag "Limit" 10))
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-height-limit nil
+  "Height (from leaves) beyond which message bodies are displayed lazily.
+
+If bound to an integer, any message with height in the message
+tree greater than this will have its body displayed lazily,
+initially only a button.
+
+If this variable is set to nil (the default) no such lazy
+display is done."
+  :type '(choice (const :tag "No limit" nil)
+                 (number :tag "Limit" 10))
   :group 'notmuch-show)
 
 (defcustom notmuch-show-relative-dates t
@@ -471,7 +518,19 @@ Return unchanged ADDRESS if parsing fails."
       ;; Otherwise format the name and address together.
       (concat p-name " <" p-address ">"))))
 
-(defun notmuch-show-insert-headerline (headers date tags depth)
+(defun notmuch-show--mark-height (tree)
+  "Calculate and cache height (distance from deepest descendent)"
+  (let* ((msg (car tree))
+        (children (cadr tree))
+        (cached-height (plist-get msg :height)))
+    (or cached-height
+       (let ((height
+              (if (null children) 0
+                (1+ (apply #'max (mapcar #'notmuch-show--mark-height children))))))
+         (plist-put msg :height height)
+         height))))
+
+(defun notmuch-show-insert-headerline (headers date tags depth duplicate file-count)
   "Insert a notmuch style headerline based on HEADERS for a
 message at DEPTH in the current thread."
   (let ((start (point))
@@ -491,7 +550,14 @@ message at DEPTH in the current thread."
            date
            ") ("
            (notmuch-tag-format-tags tags tags)
-           ")\n")
+           ")")
+    (insert
+     (if (> file-count 1)
+        (let ((txt (format "%d/%d\n" duplicate file-count)))
+          (concat
+           (notmuch-show-spaces-n (max 0 (- (window-width) (+ (current-column) (length txt)))))
+           txt))
+       "\n"))
     (overlay-put (make-overlay start (point))
                 'face 'notmuch-message-summary-face)))
 
@@ -1005,21 +1071,29 @@ is t, hide the part initially and show the button."
   (let* ((content-type (plist-get part :content-type))
         (mime-type (notmuch-show-mime-type part))
         (nth (plist-get part :id))
+        (height (plist-get msg :height))
         (long (and (notmuch-match-content-type mime-type "text/*")
                    (> notmuch-show-max-text-part-size 0)
                    (> (length (plist-get part :content))
                       notmuch-show-max-text-part-size)))
+        (deep (and notmuch-show-depth-limit
+                   (> depth notmuch-show-depth-limit)))
+        (high (and notmuch-show-height-limit
+                   (> height notmuch-show-height-limit)))
         (beg (point))
         ;; This default header-p function omits the part button for
         ;; the first (or only) part if this is text/plain.
-        (button (and (funcall notmuch-show-insert-header-p-function part hide)
+        (button (and (or deep long high
+                         (funcall notmuch-show-insert-header-p-function part hide))
                      (notmuch-show-insert-part-header
                       nth mime-type
                       (and content-type (downcase content-type))
                       (plist-get part :filename))))
-        ;; Hide the part initially if HIDE is t, or if it is too long
+        ;; Hide the part initially if HIDE is t, or if it is too long/deep
         ;; and we have a button to allow toggling.
         (show-part (not (or (equal hide t)
+                            (and deep button)
+                            (and high button)
                             (and long button))))
         (content-beg (point)))
     ;; Store the computed mime-type for later use (e.g. by attachment handlers).
@@ -1060,9 +1134,45 @@ is t, hide the part initially and show the button."
 (defvar notmuch-show-previous-subject "")
 (make-variable-buffer-local 'notmuch-show-previous-subject)
 
+(defun notmuch-show-choose-duplicate (duplicate)
+  "Display message file with index DUPLICATE in place of the current one.
+
+Message file indices are based on the order the files are
+discovered by `notmuch new' (and hence are somewhat arbitrary),
+and correspond to those passed to the \"\\-\\-duplicate\" arguments
+to the CLI.
+
+When called interactively, the function will prompt for the index
+of the file to display.  An error will be signaled if the index
+is out of range."
+  (interactive "Nduplicate: ")
+  (let ((count (length (notmuch-show-get-prop :filename))))
+    (when (or (> duplicate count)
+             (< duplicate 1))
+      (error "Duplicate %d out of range [1,%d]" duplicate count)))
+  (notmuch-show-move-to-message-top)
+  (save-excursion
+    (let* ((extent (notmuch-show-message-extent))
+          (id (notmuch-show-get-message-id))
+          (depth (notmuch-show-get-depth))
+          (inhibit-read-only t)
+          (new-msg (notmuch--run-show (list id) duplicate)))
+      ;; clean up existing overlays to avoid extending them.
+      (dolist (o (overlays-in (car extent) (cdr extent)))
+       (delete-overlay o))
+      ;; pretend insertion is happening at end of buffer
+      (narrow-to-region (point-min) (car extent))
+      ;; Insert first, then delete, to avoid marker for start of next
+      ;; message being in same place as the start of this one.
+      (notmuch-show-insert-msg new-msg depth)
+      (widen)
+      (delete-region (point) (cdr extent)))))
+
 (defun notmuch-show-insert-msg (msg depth)
   "Insert the message MSG at depth DEPTH in the current thread."
   (let* ((headers (plist-get msg :headers))
+        (duplicate (or (plist-get msg :duplicate) 0))
+        (files (length (plist-get msg :filename)))
         ;; Indentation causes the buffer offset of the start/end
         ;; points to move, so we must use markers.
         message-start message-end
@@ -1074,7 +1184,7 @@ is t, hide the part initially and show the button."
                                    (or (and notmuch-show-relative-dates
                                             (plist-get msg :date_relative))
                                        (plist-get headers :Date))
-                                   (plist-get msg :tags) depth)
+                                   (plist-get msg :tags) depth duplicate files)
     (setq content-start (point-marker))
     ;; Set `headers-start' to point after the 'Subject:' header to be
     ;; compatible with the existing implementation. This just sets it
@@ -1163,6 +1273,7 @@ is t, hide the part initially and show the button."
        (replies (cadr tree)))
     ;; We test whether there is a message or just some replies.
     (when msg
+      (notmuch-show--mark-height tree)
       (notmuch-show-insert-msg msg depth))
     (notmuch-show-insert-thread replies (1+ depth))))
 
@@ -1264,14 +1375,8 @@ matched."
   (let ((buffer-name (generate-new-buffer-name
                      (or buffer-name
                          (concat "*notmuch-" thread-id "*"))))
-       ;; We override mm-inline-override-types to stop application/*
-       ;; parts from being displayed unless the user has customized
-       ;; it themselves.
-       (mm-inline-override-types
-        (if (equal mm-inline-override-types
-                   (eval (car (get 'mm-inline-override-types 'standard-value))))
-            (cons "application/*" mm-inline-override-types)
-          mm-inline-override-types)))
+       (mm-inline-override-types (notmuch--inline-override-types)))
+
     (pop-to-buffer-same-window (get-buffer-create buffer-name))
     ;; No need to track undo information for this buffer.
     (setq buffer-undo-list t)
@@ -1319,6 +1424,18 @@ fallback if the prior matches no messages."
       (push (list thread "and (" context ")") queries))
     queries))
 
+(defun notmuch-show--header-line-format ()
+  "Compute the header line format of a notmuch-show buffer."
+  (when notmuch-show-header-line
+    (let* ((s (notmuch-sanitize
+              (notmuch-show-strip-re (notmuch-show-get-subject))))
+          (subject (replace-regexp-in-string "%" "%%" s)))
+      (cond ((stringp notmuch-show-header-line)
+             (format-spec notmuch-show-header-line `((?s . ,subject))))
+           ((functionp notmuch-show-header-line)
+            (funcall notmuch-show-header-line subject))
+           (notmuch-show-header-line subject)))))
+
 (defun notmuch-show--build-buffer (&optional state)
   "Display messages matching the current buffer context.
 
@@ -1338,7 +1455,7 @@ If no messages match the query return NIL."
         (notmuch-show-previous-subject ""))
     ;; Use results from the first query that returns some.
     (while (and (not forest) queries)
-      (setq forest (notmuch-query-get-threads
+      (setq forest (notmuch--run-show
                    (append cli-args (list "'") (car queries) (list "'"))))
       (when (and forest notmuch-show-single-message)
        (setq forest (list (list (list forest)))))
@@ -1349,13 +1466,7 @@ If no messages match the query return NIL."
       ;; display changes.
       (notmuch-show-mapc
        (lambda () (notmuch-show-set-prop :orig-tags (notmuch-show-get-tags))))
-      ;; Set the header line to the subject of the first message.
-      (when notmuch-show-header-line
-       (setq header-line-format
-             (replace-regexp-in-string "%" "%%"
-                                       (notmuch-sanitize
-                                        (notmuch-show-strip-re
-                                         (notmuch-show-get-subject))))))
+      (setq header-line-format (notmuch-show--header-line-format))
       (run-hooks 'notmuch-show-hook)
       (if state
          (notmuch-show-apply-state state)
@@ -1427,6 +1538,7 @@ non-nil) then the state of the buffer (open/closed messages) is
 reset based on the original query."
   (interactive "P")
   (let ((inhibit-read-only t)
+       (mm-inline-override-types (notmuch--inline-override-types))
        (state (unless reset-state
                 (notmuch-show-capture-state))))
     ;; `erase-buffer' does not seem to remove overlays, which can lead
@@ -1515,6 +1627,7 @@ reset based on the original query."
     (define-key map "#" 'notmuch-show-print-message)
     (define-key map "!" 'notmuch-show-toggle-elide-non-matching)
     (define-key map "$" 'notmuch-show-toggle-process-crypto)
+    (define-key map "%" 'notmuch-show-choose-duplicate)
     (define-key map "<" 'notmuch-show-toggle-thread-indentation)
     (define-key map "t" 'toggle-truncate-lines)
     (define-key map "." 'notmuch-show-part-map)
@@ -1709,10 +1822,10 @@ current thread."
 
 ;; dme: Would it make sense to use a macro for many of these?
 
-;; XXX TODO figure out what to do about multiple filenames
 (defun notmuch-show-get-filename ()
   "Return the filename of the current message."
-  (car (notmuch-show-get-prop :filename)))
+  (let ((duplicate (notmuch-show-get-duplicate)))
+    (nth (1- duplicate) (notmuch-show-get-prop :filename))))
 
 (defun notmuch-show-get-header (header &optional props)
   "Return the named header of the current message, if any."
@@ -1724,6 +1837,10 @@ current thread."
 (defun notmuch-show-get-date ()
   (notmuch-show-get-header :Date))
 
+(defun notmuch-show-get-duplicate ()
+  ;; if no duplicate property exists, assume first file
+  (or (notmuch-show-get-prop :duplicate) 1))
+
 (defun notmuch-show-get-timestamp ()
   (notmuch-show-get-prop :timestamp))
 
@@ -1918,13 +2035,15 @@ any effects from previous calls to
 (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))
+  (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender t
+                        (notmuch-show-get-prop :duplicate)))
 
 (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))
+  (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender nil
+                        (notmuch-show-get-prop :duplicate)))
 
 (put 'notmuch-show-forward-message 'notmuch-prefix-doc
      "... and prompt for sender")
@@ -2035,12 +2154,16 @@ to show, nil otherwise."
   "View the original source of the current message."
   (interactive)
   (let* ((id (notmuch-show-get-message-id))
-        (buf (get-buffer-create (concat "*notmuch-raw-" id "*")))
+        (duplicate (notmuch-show-get-duplicate))
+        (args (if (> duplicate 1)
+                  (list (format "--duplicate=%d" duplicate) id)
+                (list id)))
+        (buf (get-buffer-create (format "*notmuch-raw-%s-%d*" id duplicate)))
         (inhibit-read-only t))
     (pop-to-buffer-same-window buf)
     (erase-buffer)
     (let ((coding-system-for-read 'no-conversion))
-      (notmuch--call-process notmuch-command nil t nil "show" "--format=raw" id))
+      (apply #'notmuch--call-process notmuch-command nil t nil "show" "--format=raw" args))
     (goto-char (point-min))
     (set-buffer-modified-p nil)
     (setq buffer-read-only t)
@@ -2205,7 +2328,9 @@ argument, hide all of the messages."
 If SHOW is non-nil, open the next item in a show
 buffer. Otherwise just highlight the next item in the search
 buffer. If PREVIOUS is non-nil, move to the previous item in the
-search results instead."
+search results instead.
+
+Return non-nil on success."
   (interactive "P")
   (let ((parent-buffer notmuch-show-parent-buffer))
     (notmuch-bury-or-kill-this-buffer)
index 303c6fadcdb2da81cc0189bfe062d8b77b6bd753..f63ac9a518ea43b6b0b62687f05b1f4a77f8cdf7 100644 (file)
@@ -27,7 +27,6 @@
 (require 'mail-parse)
 
 (require 'notmuch-lib)
-(require 'notmuch-query)
 (require 'notmuch-show)
 (require 'notmuch-tag)
 (require 'notmuch-parser)
@@ -98,6 +97,15 @@ different kind of arrow point."
   :type '(alist :key-type symbol :value-type string)
   :group 'notmuch-tree)
 
+(defconst notmuch-tree--field-names
+  '(choice :tag "Field"
+          (const :tag "Date" "date")
+          (const :tag "Authors" "authors")
+          (const :tag "Subject" "subject")
+          (const :tag "Tree" "tree")
+          (const :tag "Tags" "tags")
+          (function)))
+
 (defcustom notmuch-tree-result-format
   `(("date" . "%12s  ")
     ("authors" . "%-20s")
@@ -107,7 +115,11 @@ different kind of arrow point."
     ("tags" . "(%s)"))
   "Result formatting for tree view.
 
-Supported fields are: date, authors, subject, tree, tags.
+List of pairs of (field . format-string).  Supported field
+strings are: \"date\", \"authors\", \"subject\", \"tree\",
+\"tags\".  It is also supported to pass a function in place of a
+field-name. In this case the function is passed the thread
+object (plist) and format string.
 
 Tree means the thread tree box graphics. The field may
 also be a list in which case the formatting rules are
@@ -115,14 +127,12 @@ applied recursively and then the output of all the fields
 in the list is inserted according to format-string.
 
 Note that 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 (choice string
-                                 (alist :key-type string
-                                        :value-type string))
-               :value-type string)
+\(put it in the neighbouring fields instead)."
+
+  :type `(alist :key-type (choice ,notmuch-tree--field-names
+                                 (alist :key-type ,notmuch-tree--field-names
+                                        :value-type (string :tag "Format")))
+               :value-type (string :tag "Format"))
   :group 'notmuch-tree)
 
 (defcustom notmuch-unthreaded-result-format
@@ -132,7 +142,11 @@ Note that the author string should not contain whitespace
     ("tags" . "(%s)"))
   "Result formatting for unthreaded tree view.
 
-Supported fields are: date, authors, subject, tree, tags.
+List of pairs of (field . format-string).  Supported field
+strings are: \"date\", \"authors\", \"subject\", \"tree\",
+\"tags\".  It is also supported to pass a function in place of a
+field-name. In this case the function is passed the thread
+object (plist) and format string.
 
 Tree means the thread tree box graphics. The field may
 also be a list in which case the formatting rules are
@@ -140,14 +154,12 @@ applied recursively and then the output of all the fields
 in the list is inserted according to format-string.
 
 Note that the author string should not contain whitespace
-\(put it in the neighbouring fields instead). For example:
-    (setq notmuch-unthreaded-result-format
-          '((\"authors\" . \"%-40s\")
-            (\"subject\" . \"%s\")))"
-  :type '(alist :key-type (choice string
-                                 (alist :key-type string
-                                        :value-type string))
-               :value-type string)
+\(put it in the neighbouring fields instead)."
+
+  :type `(alist :key-type (choice ,notmuch-tree--field-names
+                                 (alist :key-type ,notmuch-tree--field-names
+                                        :value-type (string :tag "Format")))
+               :value-type (string :tag "Format"))
   :group 'notmuch-tree)
 
 (defun notmuch-tree-result-format ()
index c9cf80dc619e2a60f96473abee13beca23236d69..5cb7acd298ae64899d8f7fcf4513627fdcf556cb 100644 (file)
     ("tags" . "(%s)"))
   "Search result formatting.
 
-Supported fields are: date, count, authors, subject, tags.
-For example:
-    (setq notmuch-search-result-format
-          \\='((\"authors\" . \"%-40s\")
-            (\"subject\" . \"%s\")))
+List of pairs of (field . format-string).  Supported field
+strings are: \"date\", \"count\", \"authors\", \"subject\",
+\"tags\".  It is also supported to pass a function in place of a
+field name. In this case the function is passed the thread
+object (plist) and format string.
 
 Line breaks are permitted in format strings (though this is
 currently experimental).  Note that a line break at the end of an
@@ -102,7 +102,16 @@ currently experimental).  Note that a line break at the end of an
 place it instead at the beginning of the following field.  To
 enter a line break when setting this variable with setq, use \\n.
 To enter a line break in customize, press \\[quoted-insert] C-j."
-  :type '(alist :key-type string :value-type string)
+  :type '(alist
+         :key-type
+         (choice
+          (const :tag "Date" "date")
+          (const :tag "Count" "count")
+          (const :tag "Authors" "authors")
+          (const :tag "Subject" "subject")
+          (const :tag "Tags" "tags")
+          function)
+         :value-type (string :tag "Format"))
   :group 'notmuch-search)
 
 ;; The name of this variable `notmuch-init-file' is consistent with the
@@ -520,7 +529,9 @@ no messages in the region then return nil."
 
 With a prefix argument, invert the default value of
 `notmuch-show-only-matching-messages' when displaying the
-thread."
+thread.
+
+Return non-nil on success."
   (interactive "P")
   (let ((thread-id (notmuch-search-find-thread-id)))
     (if thread-id
@@ -532,7 +543,8 @@ thread."
                      (format "*%s*" (truncate-string-to-width
                                      (notmuch-search-find-subject)
                                      30 nil nil t)))
-      (message "End of search results."))))
+      (message "End of search results.")
+      nil)))
 
 (defun notmuch-tree-from-search-current-query ()
   "Tree view of current query."
diff --git a/hooks.c b/hooks.c
index ec89b22ec75497ee69a5d9cce888c6c2a1a95c81..0cf72e748bc983d60aa09a074375b5fef2c96d97 100644 (file)
--- a/hooks.c
+++ b/hooks.c
@@ -27,6 +27,7 @@ int
 notmuch_run_hook (notmuch_database_t *notmuch, const char *hook)
 {
     char *hook_path;
+    const char *config_path;
     int status = 0;
     pid_t pid;
 
@@ -38,6 +39,12 @@ notmuch_run_hook (notmuch_database_t *notmuch, const char *hook)
        return 1;
     }
 
+    config_path = notmuch_config_path (notmuch);
+    if (setenv ("NOTMUCH_CONFIG", config_path, 1)) {
+       perror ("setenv");
+       return 1;
+    }
+
     /* Check access before fork() for speed and simplicity of error handling. */
     if (access (hook_path, X_OK) == -1) {
        /* Ignore ENOENT. It's okay not to have a hook, hook dir, or even
index df83e2048673b235e6c1055ccb47d39923f480a7..c05d70d34842f1fd1f264cc61a45fbb6348b95ca 100644 (file)
@@ -476,6 +476,11 @@ _notmuch_database_ensure_writable (notmuch_database_t *notmuch)
        return NOTMUCH_STATUS_READ_ONLY_DATABASE;
     }
 
+    if (! notmuch->open) {
+       _notmuch_database_log (notmuch, "Cannot write to a closed database.\n");
+       return NOTMUCH_STATUS_CLOSED_DATABASE;
+    }
+
     return NOTMUCH_STATUS_SUCCESS;
 }
 
@@ -852,9 +857,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch,
     notmuch_query_t *query = NULL;
     unsigned int count = 0, total = 0;
 
-    status = _notmuch_database_ensure_writable (notmuch);
-    if (status)
-       return status;
+    if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_WRITE)
+       return NOTMUCH_STATUS_READ_ONLY_DATABASE;
 
     db = notmuch->writable_xapian_db;
 
index 63b216b638127c3ccc1b92bfdfc5d89cbed5c753..1c87f8c0c2eb16ecb6b0f372f22e01072ff4fcae 100644 (file)
@@ -169,6 +169,7 @@ _notmuch_message_create_for_document (const void *talloc_owner,
 
     message->doc = doc;
     message->termpos = 0;
+    message->modified = false;
 
     return message;
 }
@@ -340,23 +341,6 @@ _notmuch_message_get_term (notmuch_message_t *message,
     return value;
 }
 
-/*
- * For special applications where we only want the thread id, reading
- * in all metadata is a heavy I/O penalty.
- */
-const char *
-_notmuch_message_get_thread_id_only (notmuch_message_t *message)
-{
-
-    Xapian::TermIterator i = message->doc.termlist_begin ();
-    Xapian::TermIterator end = message->doc.termlist_end ();
-
-    message->thread_id = _notmuch_message_get_term (message, i, end,
-                                                   _find_prefix ("thread"));
-    return message->thread_id;
-}
-
-
 static void
 _notmuch_message_ensure_metadata (notmuch_message_t *message, void *field)
 {
@@ -808,7 +792,7 @@ is_maildir (const char *p)
 }
 
 /* Add "folder:" term for directory. */
-static notmuch_status_t
+NODISCARD static notmuch_status_t
 _notmuch_message_add_folder_terms (notmuch_message_t *message,
                                   const char *directory)
 {
@@ -844,7 +828,10 @@ _notmuch_message_add_folder_terms (notmuch_message_t *message,
        *folder = '\0';
     }
 
-    _notmuch_message_add_term (message, "folder", folder);
+    if (notmuch_status_t status = COERCE_STATUS (_notmuch_message_add_term (message, "folder",
+                                                                           folder),
+                                                "adding folder term"))
+       return status;
 
     talloc_free (folder);
 
@@ -855,12 +842,17 @@ _notmuch_message_add_folder_terms (notmuch_message_t *message,
 #define RECURSIVE_SUFFIX "/**"
 
 /* Add "path:" terms for directory. */
-static notmuch_status_t
+NODISCARD static notmuch_status_t
 _notmuch_message_add_path_terms (notmuch_message_t *message,
                                 const char *directory)
 {
+    notmuch_status_t status;
+
     /* Add exact "path:" term. */
-    _notmuch_message_add_term (message, "path", directory);
+    status = COERCE_STATUS (_notmuch_message_add_term (message, "path", directory),
+                           "adding path term");
+    if (status)
+       return status;
 
     if (strlen (directory)) {
        char *path, *p;
@@ -873,7 +865,10 @@ _notmuch_message_add_path_terms (notmuch_message_t *message,
        for (p = path + strlen (path) - 1; p > path; p--) {
            if (*p == '/') {
                strcpy (p, RECURSIVE_SUFFIX);
-               _notmuch_message_add_term (message, "path", path);
+               status = COERCE_STATUS (_notmuch_message_add_term (message, "path", path),
+                                       "adding path term");
+               if (status)
+                   return status;
            }
        }
 
@@ -881,7 +876,10 @@ _notmuch_message_add_path_terms (notmuch_message_t *message,
     }
 
     /* Recursive all-matching path:** for consistency. */
-    _notmuch_message_add_term (message, "path", "**");
+    status = COERCE_STATUS (_notmuch_message_add_term (message, "path", "**"),
+                           "adding path term");
+    if (status)
+       return status;
 
     return NOTMUCH_STATUS_SUCCESS;
 }
@@ -900,6 +898,7 @@ _notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message)
        const char *direntry, *directory;
        char *colon;
        const std::string &term = *i;
+       notmuch_status_t term_status;
 
        /* Terminate loop at first term without desired prefix. */
        if (strncmp (term.c_str (), direntry_prefix, direntry_prefix_len))
@@ -920,8 +919,13 @@ _notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message)
                                                          message->notmuch,
                                                          directory_id);
 
-       _notmuch_message_add_folder_terms (message, directory);
-       _notmuch_message_add_path_terms (message, directory);
+       term_status = _notmuch_message_add_folder_terms (message, directory);
+       if (term_status)
+           return term_status;
+
+       term_status = _notmuch_message_add_path_terms (message, directory);
+       if (term_status)
+           return term_status;
     }
 
     return status;
@@ -960,10 +964,18 @@ _notmuch_message_add_filename (notmuch_message_t *message,
 
     /* New file-direntry allows navigating to this message with
      * notmuch_directory_get_child_files() . */
-    _notmuch_message_add_term (message, "file-direntry", direntry);
+    status = COERCE_STATUS (_notmuch_message_add_term (message, "file-direntry", direntry),
+                           "adding file-direntry term");
+    if (status)
+       return status;
 
-    _notmuch_message_add_folder_terms (message, directory);
-    _notmuch_message_add_path_terms (message, directory);
+    status = _notmuch_message_add_folder_terms (message, directory);
+    if (status)
+       return status;
+
+    status = _notmuch_message_add_path_terms (message, directory);
+    if (status)
+       return status;
 
     talloc_free (local);
 
@@ -1482,31 +1494,37 @@ _notmuch_message_close (notmuch_message_t *message)
  *
  * This change will not be reflected in the database until the next
  * call to _notmuch_message_sync. */
-notmuch_private_status_t
+NODISCARD notmuch_private_status_t
 _notmuch_message_add_term (notmuch_message_t *message,
                           const char *prefix_name,
                           const char *value)
 {
 
     char *term;
+    notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
 
     if (value == NULL)
        return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
 
     term = talloc_asprintf (message, "%s%s",
                            _find_prefix (prefix_name), value);
+    if (strlen (term) > NOTMUCH_TERM_MAX) {
+       status = NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
+       goto DONE;
+    }
 
-    if (strlen (term) > NOTMUCH_TERM_MAX)
-       return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
-
-    message->doc.add_term (term, 0);
-    message->modified = true;
+    try {
+       message->doc.add_term (term, 0);
+       message->modified = true;
+       _notmuch_message_invalidate_metadata (message, prefix_name);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
+    }
 
+  DONE:
     talloc_free (term);
-
-    _notmuch_message_invalidate_metadata (message, prefix_name);
-
-    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+    return status;
 }
 
 /* Parse 'text' and add a term to 'message' for each parsed word. Each
@@ -1551,7 +1569,7 @@ _notmuch_message_gen_terms (notmuch_message_t *message,
  *
  * This change will not be reflected in the database until the next
  * call to _notmuch_message_sync. */
-notmuch_private_status_t
+NODISCARD notmuch_private_status_t
 _notmuch_message_remove_term (notmuch_message_t *message,
                              const char *prefix_name,
                              const char *value)
@@ -1570,11 +1588,12 @@ _notmuch_message_remove_term (notmuch_message_t *message,
     try {
        message->doc.remove_term (term);
        message->modified = true;
-    } catch (const Xapian::InvalidArgumentError) {
+    } catch (const Xapian::InvalidArgumentError &error) {
        /* We'll let the philosophers try to wrestle with the
         * question of whether failing to remove that which was not
         * there in the first place is failure. For us, we'll silently
         * consider it all good. */
+       LOG_XAPIAN_EXCEPTION (message, error);
     }
 
     talloc_free (term);
@@ -2020,6 +2039,10 @@ notmuch_message_tags_to_maildir_flags (notmuch_message_t *message)
     char *to_set, *to_clear;
     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
 
+    status = _notmuch_database_ensure_writable (message->notmuch);
+    if (status)
+       return status;
+
     _get_maildir_flag_actions (message, &to_set, &to_clear);
 
     for (filenames = notmuch_message_get_filenames (message);
@@ -2283,7 +2306,11 @@ notmuch_message_reindex (notmuch_message_t *message,
        if (thread_id == NULL)
            thread_id = orig_thread_id;
 
-       _notmuch_message_add_term (message, "thread", thread_id);
+       ret = COERCE_STATUS (_notmuch_message_add_term (message, "thread", thread_id),
+                            "adding thread term");
+       if (ret)
+           goto DONE;
+
        /* Take header values only from first filename */
        if (found == 0)
            _notmuch_message_set_header_values (message, date, from, subject);
@@ -2301,7 +2328,11 @@ notmuch_message_reindex (notmuch_message_t *message,
     }
     if (found == 0) {
        /* put back thread id to help cleanup */
-       _notmuch_message_add_term (message, "thread", orig_thread_id);
+       ret = COERCE_STATUS (_notmuch_message_add_term (message, "thread", orig_thread_id),
+                            "adding thread term");
+       if (ret)
+           goto DONE;
+
        ret = _notmuch_message_delete (message);
     } else {
        _notmuch_message_sync (message);
index 3cc79bc4c86148445aaaf7f831421abdbb45801d..1d3d2b0c9db220d36723728c044f6b0b8e71cfef 100644 (file)
@@ -144,6 +144,9 @@ typedef enum {
     NOTMUCH_PRIVATE_STATUS_NO_CONFIG                           = NOTMUCH_STATUS_NO_CONFIG,
     NOTMUCH_PRIVATE_STATUS_NO_DATABASE                         = NOTMUCH_STATUS_NO_DATABASE,
     NOTMUCH_PRIVATE_STATUS_DATABASE_EXISTS                     = NOTMUCH_STATUS_DATABASE_EXISTS,
+    NOTMUCH_PRIVATE_STATUS_NO_MAIL_ROOT                                = NOTMUCH_STATUS_NO_MAIL_ROOT,
+    NOTMUCH_PRIVATE_STATUS_BAD_QUERY_SYNTAX                    = NOTMUCH_STATUS_BAD_QUERY_SYNTAX,
+    NOTMUCH_PRIVATE_STATUS_CLOSED_DATABASE                     = NOTMUCH_STATUS_CLOSED_DATABASE,
 
     /* Then add our own private values. */
     NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG               = NOTMUCH_STATUS_LAST_STATUS,
@@ -586,9 +589,6 @@ _notmuch_message_add_reply (notmuch_message_t *message,
 void
 _notmuch_message_remove_unprefixed_terms (notmuch_message_t *message);
 
-const char *
-_notmuch_message_get_thread_id_only (notmuch_message_t *message);
-
 size_t _notmuch_message_get_thread_depth (notmuch_message_t *message);
 
 void
@@ -753,6 +753,12 @@ _notmuch_talloc_steal (const void *new_ctx, const T *ptr)
 #undef talloc_steal
 #define talloc_steal _notmuch_talloc_steal
 #endif
+
+#if __cplusplus >= 201703L || __cppcheck__
+#define NODISCARD [[nodiscard]]
+#else
+#define NODISCARD /**/
+#endif
 #endif
 
 #endif
index 2e6ec2af1ae2a5054cd1516088764cc44557faa4..0b0540b13437f6161adbba741352ae9df0a2cdfd 100644 (file)
@@ -228,6 +228,10 @@ typedef enum {
      * No mail root could be deduced from parameters and environment
      */
     NOTMUCH_STATUS_NO_MAIL_ROOT,
+    /**
+     * Database is not fully opened, or has been closed
+     */
+    NOTMUCH_STATUS_CLOSED_DATABASE,
     /**
      * Not an actual status value. Just a way to find out how many
      * valid status values there are.
@@ -2262,6 +2266,9 @@ notmuch_message_properties_destroy (notmuch_message_properties_t *properties);
  * valid string. Whereas when this function returns FALSE,
  * notmuch_tags_get will return NULL.
  *
+ * It is acceptable to pass NULL for 'tags', in which case this
+ * function will always return FALSE.
+
  * See the documentation of notmuch_message_get_tags for example code
  * showing how to iterate over a notmuch_tags_t object.
  */
index 08fd703708e86e6c75d960f19e2a404f46bcd20c..0f14d8b7307173981ca43f2fbac9d86f91a1b5e1 100644 (file)
@@ -187,6 +187,55 @@ _sexp_parse_phrase (std::string term_prefix, const char *phrase, Xapian::Query &
     return NOTMUCH_STATUS_SUCCESS;
 }
 
+static notmuch_status_t
+resolve_binding (notmuch_database_t *notmuch, const _sexp_binding_t *env, const char *name,
+                const _sexp_binding_t **out)
+{
+    for (; env; env = env->next) {
+       if (strcmp (name, env->name) == 0) {
+           *out = env;
+           return NOTMUCH_STATUS_SUCCESS;
+       }
+    }
+
+    _notmuch_database_log (notmuch, "undefined parameter '%s'\n", name);
+    return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+}
+
+static notmuch_status_t
+_sexp_expand_term (notmuch_database_t *notmuch,
+                  const _sexp_prefix_t *prefix,
+                  const _sexp_binding_t *env,
+                  const sexp_t *sx,
+                  const char **out)
+{
+    notmuch_status_t status;
+
+    if (! out)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    while (sx->ty == SEXP_VALUE && sx->aty == SEXP_BASIC && sx->val[0] == ',') {
+       const char *name = sx->val + 1;
+       const _sexp_binding_t *binding;
+
+       status = resolve_binding (notmuch, env, name, &binding);
+       if (status)
+           return status;
+
+       sx = binding->sx;
+       env = binding->context;
+    }
+
+    if (sx->ty != SEXP_VALUE) {
+       _notmuch_database_log (notmuch, "'%s' expects single atom as argument\n",
+                              prefix->name);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    *out = sx->val;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
 static notmuch_status_t
 _sexp_parse_wildcard (notmuch_database_t *notmuch,
                      const _sexp_prefix_t *parent,
@@ -227,8 +276,8 @@ _sexp_parse_one_term (notmuch_database_t *notmuch, std::string term_prefix, cons
 notmuch_status_t
 _sexp_parse_regex (notmuch_database_t *notmuch,
                   const _sexp_prefix_t *prefix, const _sexp_prefix_t *parent,
-                  unused(const _sexp_binding_t *env),
-                  std::string val, Xapian::Query &output)
+                  const _sexp_binding_t *env,
+                  const sexp_t *term, Xapian::Query &output)
 {
     if (! parent) {
        _notmuch_database_log (notmuch, "illegal '%s' outside field\n",
@@ -243,9 +292,15 @@ _sexp_parse_regex (notmuch_database_t *notmuch,
     }
 
     std::string msg; /* ignored */
+    const char *str;
+    notmuch_status_t status;
+
+    status = _sexp_expand_term (notmuch, prefix, env, term, &str);
+    if (status)
+       return status;
 
     return _notmuch_regexp_to_query (notmuch, Xapian::BAD_VALUENO, parent->name,
-                                    val, output, msg);
+                                    str, output, msg);
 }
 
 
@@ -444,14 +499,16 @@ _sexp_expand_param (notmuch_database_t *notmuch, const _sexp_prefix_t *parent,
                    const _sexp_binding_t *env, const char *name,
                    Xapian::Query &output)
 {
-    for (; env; env = env->next) {
-       if (strcmp (name, env->name) == 0) {
-           return _sexp_to_xapian_query (notmuch, parent, env->context, env->sx,
-                                         output);
-       }
-    }
-    _notmuch_database_log (notmuch, "undefined parameter %s\n", name);
-    return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    notmuch_status_t status;
+
+    const _sexp_binding_t *binding;
+
+    status = resolve_binding (notmuch, env, name, &binding);
+    if (status)
+       return status;
+
+    return _sexp_to_xapian_query (notmuch, parent, binding->context, binding->sx,
+                                 output);
 }
 
 static notmuch_status_t
@@ -473,6 +530,9 @@ _sexp_parse_range (notmuch_database_t *notmuch,  const _sexp_prefix_t *prefix,
     }
 
     from = sx->val;
+    if (strcmp (from, "*") == 0)
+       from = "";
+
     to = from;
 
     if (sx->next) {
@@ -488,6 +548,8 @@ _sexp_parse_range (notmuch_database_t *notmuch,  const _sexp_prefix_t *prefix,
        }
 
        to = sx->next->val;
+       if (strcmp (to, "*") == 0)
+           to = "";
     }
 
     if (strcmp (prefix->name, "date") == 0) {
@@ -504,14 +566,20 @@ _sexp_parse_range (notmuch_database_t *notmuch,  const _sexp_prefix_t *prefix,
        long from_idx, to_idx;
 
        try {
-           from_idx = std::stol (from);
+           if (EMPTY_STRING (from))
+               from_idx = 0L;
+           else
+               from_idx = std::stol (from);
        } catch (std::logic_error &e) {
            _notmuch_database_log (notmuch, "bad 'from' revision: '%s'\n", from);
            return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
        }
 
        try {
-           to_idx = std::stol (to);
+           if (EMPTY_STRING (to))
+               to_idx = LONG_MAX;
+           else
+               to_idx = std::stol (to);
        } catch (std::logic_error &e) {
            _notmuch_database_log (notmuch, "bad 'to' revision: '%s'\n", to);
            return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
@@ -638,11 +706,17 @@ _sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent
                return _notmuch_query_name_to_query (notmuch, sx->list->next->val, output);
            }
 
-           if (prefix->xapian_op == Xapian::Query::OP_WILDCARD)
-               return _sexp_parse_wildcard (notmuch, parent, env, sx->list->next->val, output);
+           if (prefix->xapian_op == Xapian::Query::OP_WILDCARD) {
+               const char *str;
+               status = _sexp_expand_term (notmuch, prefix, env, sx->list->next, &str);
+               if (status)
+                   return status;
+
+               return _sexp_parse_wildcard (notmuch, parent, env, str, output);
+           }
 
            if (prefix->flags & SEXP_FLAG_DO_REGEX) {
-               return _sexp_parse_regex (notmuch, prefix, parent, env, sx->list->next->val, output);
+               return _sexp_parse_regex (notmuch, prefix, parent, env, sx->list->next, output);
            }
 
            if (prefix->flags & SEXP_FLAG_DO_EXPAND) {
index c7d3f66f491b5ec02495817ba85b6c6981c5fc6d..ec5366ffa23681bf4ddd37a576b2ee202199b737 100644 (file)
@@ -48,7 +48,7 @@ _notmuch_tags_create (const void *ctx, notmuch_string_list_t *list)
 notmuch_bool_t
 notmuch_tags_valid (notmuch_tags_t *tags)
 {
-    return tags->iterator != NULL;
+    return tags && (tags->iterator != NULL);
 }
 
 const char *
index d29c4e48a8054055b7e49aa99fa9f83edfb897db..1c5d619b352a1601315947ee15c4b5f5dcb50dcc 100644 (file)
@@ -78,13 +78,14 @@ mime_node_get_message_crypto_status (mime_node_t *node)
 
 notmuch_status_t
 mime_node_open (const void *ctx, notmuch_message_t *message,
+               int duplicate,
                _notmuch_crypto_t *crypto, mime_node_t **root_out)
 {
     const char *filename = notmuch_message_get_filename (message);
     mime_node_context_t *mctx;
     mime_node_t *root;
     notmuch_status_t status;
-    int fd;
+    int fd = -1;
 
     root = talloc_zero (ctx, mime_node_t);
     if (root == NULL) {
@@ -103,20 +104,33 @@ mime_node_open (const void *ctx, notmuch_message_t *message,
     talloc_set_destructor (mctx, _mime_node_context_free);
 
     /* Fast path */
-    fd = open (filename, O_RDONLY);
+    if (duplicate <= 0)
+       fd = open (filename, O_RDONLY);
     if (fd == -1) {
-       /* Slow path - for some reason the first file in the list is
-        * not available anymore. This is clearly a problem in the
+       /* Slow path - Either we are trying to open a specific file, or
+        * for some reason the first file in the list is
+        * not available anymore. The latter is clearly a problem in the
         * database, but we are not going to let this problem be a
         * show stopper */
        notmuch_filenames_t *filenames;
+       int i = 1;
+
        for (filenames = notmuch_message_get_filenames (message);
             notmuch_filenames_valid (filenames);
-            notmuch_filenames_move_to_next (filenames)) {
-           filename = notmuch_filenames_get (filenames);
-           fd = open (filename, O_RDONLY);
-           if (fd != -1)
-               break;
+            notmuch_filenames_move_to_next (filenames), i++) {
+           if (i >= duplicate) {
+               filename = notmuch_filenames_get (filenames);
+               fd = open (filename, O_RDONLY);
+               if (fd != -1) {
+                   break;
+               } else {
+                   if (duplicate > 0) {
+                       fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+                       status = NOTMUCH_STATUS_FILE_ERROR;
+                       goto DONE;
+                   }
+               }
+           }
        }
 
        talloc_free (filenames);
index 9f57ac5e3935c430fb983645860780a8a937986b..21b49908ae246c1bffd0b7cc242b79b663cbb800 100644 (file)
@@ -75,6 +75,7 @@ typedef struct notmuch_show_params {
     bool entire_thread;
     bool omit_excluded;
     bool output_body;
+    int duplicate;
     int part;
     _notmuch_crypto_t crypto;
     bool include_html;
@@ -229,6 +230,7 @@ show_one_part (const char *filename, int part);
 
 void
 format_part_sprinter (const void *ctx, struct sprinter *sp, mime_node_t *node,
+                     int duplicate,
                      bool output_body,
                      bool include_html);
 
@@ -388,7 +390,8 @@ struct mime_node {
 };
 
 /* Construct a new MIME node pointing to the root message part of
- * message. If crypto->verify is true, signed child parts will be
+ * message. Use the duplicate-th filename if that parameter is
+ * positive. If crypto->verify is true, signed child parts will be
  * verified. If crypto->decrypt is NOTMUCH_DECRYPT_TRUE, encrypted
  * child parts will be decrypted using either stored session keys or
  * asymmetric crypto.  If crypto->decrypt is NOTMUCH_DECRYPT_AUTO,
@@ -406,6 +409,7 @@ struct mime_node {
  */
 notmuch_status_t
 mime_node_open (const void *ctx, notmuch_message_t *message,
+               int duplicate,
                _notmuch_crypto_t *crypto, mime_node_t **node_out);
 
 /* Return a new MIME node for the requested child part of parent.
diff --git a/notmuch-git.py b/notmuch-git.py
new file mode 100644 (file)
index 0000000..ceb86fb
--- /dev/null
@@ -0,0 +1,1218 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2011-2014 David Bremner <david@tethera.net>
+#                         W. Trevor King <wking@tremily.us>
+#
+# 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 https://www.gnu.org/licenses/ .
+
+"""
+Manage notmuch tags with Git
+"""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import codecs as _codecs
+import collections as _collections
+import functools as _functools
+import inspect as _inspect
+import locale as _locale
+import logging as _logging
+import os as _os
+import re as _re
+import shutil as _shutil
+import subprocess as _subprocess
+import sys as _sys
+import tempfile as _tempfile
+import textwrap as _textwrap
+from urllib.parse import quote as _quote
+from urllib.parse import unquote as _unquote
+import json as _json
+
+_LOG = _logging.getLogger('notmuch-git')
+_LOG.setLevel(_logging.WARNING)
+_LOG.addHandler(_logging.StreamHandler())
+
+NOTMUCH_GIT_DIR = None
+TAG_PREFIX = None
+FORMAT_VERSION = 1
+
+_HEX_ESCAPE_REGEX = _re.compile('%[0-9A-F]{2}')
+_TAG_DIRECTORY = 'tags/'
+_TAG_FILE_REGEX = ( _re.compile(_TAG_DIRECTORY + '(?P<id>[^/]*)/(?P<tag>[^/]*)'),
+                    _re.compile(_TAG_DIRECTORY + '([0-9a-f]{2}/){2}(?P<id>[^/]*)/(?P<tag>[^/]*)'))
+
+# magic hash for Git (git hash-object -t blob /dev/null)
+_EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
+
+def _hex_quote(string, safe='+@=:,'):
+    """
+    quote('abc def') -> 'abc%20def'.
+
+    Wrap urllib.parse.quote with additional safe characters (in
+    addition to letters, digits, and '_.-') and lowercase hex digits
+    (e.g. '%3a' instead of '%3A').
+    """
+    uppercase_escapes = _quote(string, safe)
+    return _HEX_ESCAPE_REGEX.sub(
+        lambda match: match.group(0).lower(),
+        uppercase_escapes)
+
+def _xapian_quote(string):
+    """
+    Quote a string for Xapian's QueryParser.
+
+    Xapian uses double-quotes for quoting strings.  You can escape
+    internal quotes by repeating them [1,2,3].
+
+    [1]: https://trac.xapian.org/ticket/128#comment:2
+    [2]: https://trac.xapian.org/ticket/128#comment:17
+    [3]: https://trac.xapian.org/changeset/13823/svn
+    """
+    return '"{0}"'.format(string.replace('"', '""'))
+
+
+def _xapian_unquote(string):
+    """
+    Unquote a Xapian-quoted string.
+    """
+    if string.startswith('"') and string.endswith('"'):
+        return string[1:-1].replace('""', '"')
+    return string
+
+
+def timed(fn):
+    """Timer decorator"""
+    from time import perf_counter
+
+    def inner(*args, **kwargs):
+        start_time = perf_counter()
+        rval = fn(*args, **kwargs)
+        end_time = perf_counter()
+        _LOG.info('{0}: {1:.8f}s elapsed'.format(fn.__name__, end_time - start_time))
+        return rval
+
+    return inner
+
+
+class SubprocessError(RuntimeError):
+    "A subprocess exited with a nonzero status"
+    def __init__(self, args, status, stdout=None, stderr=None):
+        self.status = status
+        self.stdout = stdout
+        self.stderr = stderr
+        msg = '{args} exited with {status}'.format(args=args, status=status)
+        if stderr:
+            msg = '{msg}: {stderr}'.format(msg=msg, stderr=stderr)
+        super(SubprocessError, self).__init__(msg)
+
+
+class _SubprocessContextManager(object):
+    """
+    PEP 343 context manager for subprocesses.
+
+    'expect' holds a tuple of acceptable exit codes, otherwise we'll
+    raise a SubprocessError in __exit__.
+    """
+    def __init__(self, process, args, expect=(0,)):
+        self._process = process
+        self._args = args
+        self._expect = expect
+
+    def __enter__(self):
+        return self._process
+
+    def __exit__(self, type, value, traceback):
+        for name in ['stdin', 'stdout', 'stderr']:
+            stream = getattr(self._process, name)
+            if stream:
+                stream.close()
+                setattr(self._process, name, None)
+        status = self._process.wait()
+        _LOG.debug(
+            'collect {args} with status {status} (expected {expect})'.format(
+                args=self._args, status=status, expect=self._expect))
+        if status not in self._expect:
+            raise SubprocessError(args=self._args, status=status)
+
+    def wait(self):
+        return self._process.wait()
+
+
+def _spawn(args, input=None, additional_env=None, wait=False, stdin=None,
+           stdout=None, stderr=None, encoding=_locale.getpreferredencoding(),
+           expect=(0,), **kwargs):
+    """Spawn a subprocess, and optionally wait for it to finish.
+
+    This wrapper around subprocess.Popen has two modes, depending on
+    the truthiness of 'wait'.  If 'wait' is true, we use p.communicate
+    internally to write 'input' to the subprocess's stdin and read
+    from it's stdout/stderr.  If 'wait' is False, we return a
+    _SubprocessContextManager instance for fancier handling
+    (e.g. piping between processes).
+
+    For 'wait' calls when you want to write to the subprocess's stdin,
+    you only need to set 'input' to your content.  When 'input' is not
+    None but 'stdin' is, we'll automatically set 'stdin' to PIPE
+    before calling Popen.  This avoids having the subprocess
+    accidentally inherit the launching process's stdin.
+    """
+    _LOG.debug('spawn {args} (additional env. var.: {env})'.format(
+        args=args, env=additional_env))
+    if not stdin and input is not None:
+        stdin = _subprocess.PIPE
+    if additional_env:
+        if not kwargs.get('env'):
+            kwargs['env'] = dict(_os.environ)
+        kwargs['env'].update(additional_env)
+    p = _subprocess.Popen(
+        args, stdin=stdin, stdout=stdout, stderr=stderr, **kwargs)
+    if wait:
+        if hasattr(input, 'encode'):
+            input = input.encode(encoding)
+        (stdout, stderr) = p.communicate(input=input)
+        status = p.wait()
+        _LOG.debug(
+            'collect {args} with status {status} (expected {expect})'.format(
+                args=args, status=status, expect=expect))
+        if stdout is not None:
+            stdout = stdout.decode(encoding)
+        if stderr is not None:
+            stderr = stderr.decode(encoding)
+        if status not in expect:
+            raise SubprocessError(
+                args=args, status=status, stdout=stdout, stderr=stderr)
+        return (status, stdout, stderr)
+    if p.stdin and not stdin:
+        p.stdin.close()
+        p.stdin = None
+    if p.stdin:
+        p.stdin = _codecs.getwriter(encoding=encoding)(stream=p.stdin)
+    stream_reader = _codecs.getreader(encoding=encoding)
+    if p.stdout:
+        p.stdout = stream_reader(stream=p.stdout)
+    if p.stderr:
+        p.stderr = stream_reader(stream=p.stderr)
+    return _SubprocessContextManager(args=args, process=p, expect=expect)
+
+
+def _git(args, **kwargs):
+    args = ['git', '--git-dir', NOTMUCH_GIT_DIR] + list(args)
+    return _spawn(args=args, **kwargs)
+
+
+def _get_current_branch():
+    """Get the name of the current branch.
+
+    Return 'None' if we're not on a branch.
+    """
+    try:
+        (status, branch, stderr) = _git(
+            args=['symbolic-ref', '--short', 'HEAD'],
+            stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True)
+    except SubprocessError as e:
+        if 'not a symbolic ref' in e:
+            return None
+        raise
+    return branch.strip()
+
+
+def _get_remote():
+    "Get the default remote for the current branch."
+    local_branch = _get_current_branch()
+    (status, remote, stderr) = _git(
+        args=['config', 'branch.{0}.remote'.format(local_branch)],
+        stdout=_subprocess.PIPE, wait=True)
+    return remote.strip()
+
+def _tag_query(prefix=None):
+    if prefix is None:
+        prefix = TAG_PREFIX
+    return '(tag (starts-with "{:s}"))'.format(prefix.replace('"','\\\"'))
+
+def count_messages(prefix=None):
+    "count messages with a given prefix."
+    (status, stdout, stderr) = _spawn(
+        args=['notmuch', 'count', '--query=sexp', _tag_query(prefix)],
+        stdout=_subprocess.PIPE, wait=True)
+    if status != 0:
+        _LOG.error("failed to run notmuch config")
+        sys.exit(1)
+    return int(stdout.rstrip())
+
+def get_tags(prefix=None):
+    "Get a list of tags with a given prefix."
+    (status, stdout, stderr) = _spawn(
+        args=['notmuch', 'search', '--query=sexp', '--output=tags', _tag_query(prefix)],
+        stdout=_subprocess.PIPE, wait=True)
+    return [tag for tag in stdout.splitlines()]
+
+def archive(treeish='HEAD', args=()):
+    """
+    Dump a tar archive of the current notmuch-git tag set.
+
+    Using 'git archive'.
+
+    Each tag $tag for message with Message-Id $id is written to
+    an empty file
+
+      tags/hash1(id)/hash2(id)/encode($id)/encode($tag)
+
+    The encoding preserves alphanumerics, and the characters
+    "+-_@=.:," (not the quotes).  All other octets are replaced with
+    '%' followed by a two digit hex number.
+    """
+    _git(args=['archive', treeish] + list(args), wait=True)
+
+
+def clone(repository):
+    """
+    Create a local notmuch-git repository from a remote source.
+
+    This wraps 'git clone', adding some options to avoid creating a
+    working tree while preserving remote-tracking branches and
+    upstreams.
+    """
+    with _tempfile.TemporaryDirectory(prefix='notmuch-git-clone.') as workdir:
+        _spawn(
+            args=[
+                'git', 'clone', '--no-checkout', '--separate-git-dir', NOTMUCH_GIT_DIR,
+                repository, workdir],
+            wait=True)
+    _git(args=['config', '--unset', 'core.worktree'], wait=True, expect=(0, 5))
+    _git(args=['config', 'core.bare', 'true'], wait=True)
+    (status, stdout, stderr) = _git(args=['show-ref', '--verify',
+                                          '--quiet',
+                                          'refs/remotes/origin/config'],
+                                    expect=(0,1),
+                                    wait=True)
+    if status == 0:
+        _git(args=['branch', 'config', 'origin/config'], wait=True)
+    existing_tags = get_tags()
+    if existing_tags:
+        _LOG.warning(
+            'Not checking out to avoid clobbering existing tags: {}'.format(
+            ', '.join(existing_tags)))
+    else:
+        checkout()
+
+
+def _is_committed(status):
+    return len(status['added']) + len(status['deleted']) == 0
+
+
+class CachedIndex:
+    def __init__(self, repo, treeish):
+        self.cache_path = _os.path.join(repo, 'notmuch', 'index_cache.json')
+        self.index_path = _os.path.join(repo, 'index')
+        self.current_treeish = treeish
+        # cached values
+        self.treeish = None
+        self.hash = None
+        self.index_checksum = None
+
+        self._load_cache_file()
+
+    def _load_cache_file(self):
+        try:
+            with open(self.cache_path) as f:
+                data = _json.load(f)
+                self.treeish = data['treeish']
+                self.hash = data['hash']
+                self.index_checksum = data['index_checksum']
+        except FileNotFoundError:
+            pass
+        except _json.JSONDecodeError:
+            _LOG.error("Error decoding cache")
+            _sys.exit(1)
+
+    def __enter__(self):
+        self.read_tree()
+        return self
+
+    def __exit__(self, type, value, traceback):
+        checksum = _read_index_checksum(self.index_path)
+        (_, hash, _) = _git(
+            args=['rev-parse', self.current_treeish],
+            stdout=_subprocess.PIPE,
+            wait=True)
+
+        with open(self.cache_path, "w") as f:
+            _json.dump({'treeish': self.current_treeish,
+                        'hash': hash.rstrip(),  'index_checksum': checksum }, f)
+
+    @timed
+    def read_tree(self):
+        current_checksum = _read_index_checksum(self.index_path)
+        (_, hash, _) = _git(
+            args=['rev-parse', self.current_treeish],
+            stdout=_subprocess.PIPE,
+            wait=True)
+        current_hash = hash.rstrip()
+
+        if self.current_treeish == self.treeish and \
+           self.index_checksum and self.index_checksum == current_checksum and \
+           self.hash and self.hash == current_hash:
+            return
+
+        _git(args=['read-tree', self.current_treeish], wait=True)
+
+
+def check_safe_fraction(status):
+    safe = 0.1
+    conf = _notmuch_config_get ('git.safe_fraction')
+    if conf and conf != '':
+        safe=float(conf)
+
+    total = count_messages (TAG_PREFIX)
+    if total == 0:
+        _LOG.error('No existing tags with given prefix, stopping.'.format(safe))
+        _LOG.error('Use --force to override.')
+        exit(1)
+    change = len(status['added'])+len(status['deleted'])
+    fraction = change/total
+    _LOG.debug('total messages {:d}, change: {:d}, fraction: {:f}'.format(total,change,fraction))
+    if fraction > safe:
+        _LOG.error('safe fraction {:f} exceeded, stopping.'.format(safe))
+        _LOG.error('Use --force to override or reconfigure git.safe_fraction.')
+        exit(1)
+
+def commit(treeish='HEAD', message=None, force=False):
+    """
+    Commit prefix-matching tags from the notmuch database to Git.
+    """
+
+    status = get_status()
+
+    if _is_committed(status=status):
+        _LOG.warning('Nothing to commit')
+        return
+
+    if not force:
+        check_safe_fraction (status)
+
+    with CachedIndex(NOTMUCH_GIT_DIR, treeish) as index:
+        try:
+            _update_index(status=status)
+            (_, tree, _) = _git(
+                args=['write-tree'],
+                stdout=_subprocess.PIPE,
+                wait=True)
+            (_, parent, _) = _git(
+                args=['rev-parse', treeish],
+                stdout=_subprocess.PIPE,
+                wait=True)
+            (_, commit, _) = _git(
+                args=['commit-tree', tree.strip(), '-p', parent.strip()],
+                input=message,
+                stdout=_subprocess.PIPE,
+                wait=True)
+            _git(
+                args=['update-ref', treeish, commit.strip()],
+                stdout=_subprocess.PIPE,
+                wait=True)
+        except Exception as e:
+            _git(args=['read-tree', '--empty'], wait=True)
+            _git(args=['read-tree', treeish], wait=True)
+            raise
+
+@timed
+def _update_index(status):
+    with _git(
+            args=['update-index', '--index-info'],
+            stdin=_subprocess.PIPE) as p:
+        for id, tags in status['deleted'].items():
+            for line in _index_tags_for_message(id=id, status='D', tags=tags):
+                p.stdin.write(line)
+        for id, tags in status['added'].items():
+            for line in _index_tags_for_message(id=id, status='A', tags=tags):
+                p.stdin.write(line)
+
+
+def fetch(remote=None):
+    """
+    Fetch changes from the remote repository.
+
+    See 'merge' to bring those changes into notmuch.
+    """
+    args = ['fetch']
+    if remote:
+        args.append(remote)
+    _git(args=args, wait=True)
+
+
+def init(remote=None,format_version=None):
+    """
+    Create an empty notmuch-git repository.
+
+    This wraps 'git init' with a few extra steps to support subsequent
+    status and commit commands.
+    """
+    from pathlib import Path
+    parent = Path(NOTMUCH_GIT_DIR).parent
+    try:
+        _os.makedirs(parent)
+    except FileExistsError:
+        pass
+
+    if not format_version:
+        format_version = 1
+
+    format_version=int(format_version)
+
+    if format_version > 1 or format_version < 0:
+        _LOG.error("Illegal format version {:d}".format(format_version))
+        _sys.exit(1)
+
+    _spawn(args=['git', '--git-dir', NOTMUCH_GIT_DIR, 'init',
+                 '--initial-branch=master', '--quiet', '--bare'], wait=True)
+    _git(args=['config', 'core.logallrefupdates', 'true'], wait=True)
+    # create an empty blob (e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)
+    _git(args=['hash-object', '-w', '--stdin'], input='', wait=True)
+    allow_empty=('--allow-empty',)
+    if format_version >= 1:
+        allow_empty=()
+        # create a blob for the FORMAT file
+        (status, stdout, _) = _git(args=['hash-object', '-w', '--stdin'], stdout=_subprocess.PIPE,
+                                   input='{:d}\n'.format(format_version), wait=True)
+        verhash=stdout.rstrip()
+        _LOG.debug('hash of FORMAT blob = {:s}'.format(verhash))
+        # Add FORMAT to the index
+        _git(args=['update-index', '--add', '--cacheinfo', '100644,{:s},FORMAT'.format(verhash)], wait=True)
+
+    _git(
+        args=[
+            'commit', *allow_empty, '-m', 'Start a new notmuch-git repository'
+        ],
+        additional_env={'GIT_WORK_TREE': NOTMUCH_GIT_DIR},
+        wait=True)
+
+
+def checkout(force=None):
+    """
+    Update the notmuch database from Git.
+
+    This is mainly useful to discard your changes in notmuch relative
+    to Git.
+    """
+    status = get_status()
+
+    if not force:
+        check_safe_fraction(status)
+
+    with _spawn(
+            args=['notmuch', 'tag', '--batch'], stdin=_subprocess.PIPE) as p:
+        for id, tags in status['added'].items():
+            p.stdin.write(_batch_line(action='-', id=id, tags=tags))
+        for id, tags in status['deleted'].items():
+            p.stdin.write(_batch_line(action='+', id=id, tags=tags))
+
+
+def _batch_line(action, id, tags):
+    """
+    'notmuch tag --batch' line for adding/removing tags.
+
+    Set 'action' to '-' to remove a tag or '+' to add the tags to a
+    given message id.
+    """
+    tag_string = ' '.join(
+        '{action}{prefix}{tag}'.format(
+            action=action, prefix=_ENCODED_TAG_PREFIX, tag=_hex_quote(tag))
+        for tag in tags)
+    line = '{tags} -- id:{id}\n'.format(
+        tags=tag_string, id=_xapian_quote(string=id))
+    return line
+
+
+def _insist_committed():
+    "Die if the the notmuch tags don't match the current HEAD."
+    status = get_status()
+    if not _is_committed(status=status):
+        _LOG.error('\n'.join([
+            'Uncommitted changes to {prefix}* tags in notmuch',
+            '',
+            "For a summary of changes, run 'notmuch-git status'",
+            "To save your changes,     run 'notmuch-git commit' before merging/pull",
+            "To discard your changes,  run 'notmuch-git checkout'",
+            ]).format(prefix=TAG_PREFIX))
+        _sys.exit(1)
+
+
+def pull(repository=None, refspecs=None):
+    """
+    Pull (merge) remote repository changes to notmuch.
+
+    'pull' is equivalent to 'fetch' followed by 'merge'.  We use the
+    Git-configured repository for your current branch
+    (branch.<name>.repository, likely 'origin', and
+    branch.<name>.merge, likely 'master').
+    """
+    _insist_committed()
+    if refspecs and not repository:
+        repository = _get_remote()
+    args = ['pull']
+    if repository:
+        args.append(repository)
+    if refspecs:
+        args.extend(refspecs)
+    with _tempfile.TemporaryDirectory(prefix='notmuch-git-pull.') as workdir:
+        for command in [
+                ['reset', '--hard'],
+                args]:
+            _git(
+                args=command,
+                additional_env={'GIT_WORK_TREE': workdir},
+                wait=True)
+    checkout()
+
+
+def merge(reference='@{upstream}'):
+    """
+    Merge changes from 'reference' into HEAD and load the result into notmuch.
+
+    The default reference is '@{upstream}'.
+    """
+    _insist_committed()
+    with _tempfile.TemporaryDirectory(prefix='notmuch-git-merge.') as workdir:
+        for command in [
+                ['reset', '--hard'],
+                ['merge', reference]]:
+            _git(
+                args=command,
+                additional_env={'GIT_WORK_TREE': workdir},
+                wait=True)
+    checkout()
+
+
+def log(args=()):
+    """
+    A simple wrapper for 'git log'.
+
+    After running 'notmuch-git fetch', you can inspect the changes with
+    'notmuch-git log HEAD..@{upstream}'.
+    """
+    # we don't want output trapping here, because we want the pager.
+    args = ['log', '--name-status', '--no-renames'] + list(args)
+    with _git(args=args, expect=(0, 1, -13)) as p:
+        p.wait()
+
+
+def push(repository=None, refspecs=None):
+    "Push the local notmuch-git Git state to a remote repository."
+    if refspecs and not repository:
+        repository = _get_remote()
+    args = ['push']
+    if repository:
+        args.append(repository)
+    if refspecs:
+        args.extend(refspecs)
+    _git(args=args, wait=True)
+
+
+def status():
+    """
+    Show pending updates in notmuch or git repo.
+
+    Prints lines of the form
+
+      ng Message-Id tag
+
+    where n is a single character representing notmuch database status
+
+    * A
+
+      Tag is present in notmuch database, but not committed to notmuch-git
+      (equivalently, tag has been deleted in notmuch-git repo, e.g. by a
+      pull, but not restored to notmuch database).
+
+    * D
+
+      Tag is present in notmuch-git repo, but not restored to notmuch
+      database (equivalently, tag has been deleted in notmuch).
+
+    * U
+
+      Message is unknown (missing from local notmuch database).
+
+    The second character (if present) represents a difference between
+    local and upstream branches. Typically 'notmuch-git fetch' needs to be
+    run to update this.
+
+    * a
+
+      Tag is present in upstream, but not in the local Git branch.
+
+    * d
+
+      Tag is present in local Git branch, but not upstream.
+    """
+    status = get_status()
+    # 'output' is a nested defaultdict for message status:
+    # * The outer dict is keyed by message id.
+    # * The inner dict is keyed by tag name.
+    # * The inner dict values are status strings (' a', 'Dd', ...).
+    output = _collections.defaultdict(
+        lambda : _collections.defaultdict(lambda : ' '))
+    for id, tags in status['added'].items():
+        for tag in tags:
+            output[id][tag] = 'A'
+    for id, tags in status['deleted'].items():
+        for tag in tags:
+            output[id][tag] = 'D'
+    for id, tags in status['missing'].items():
+        for tag in tags:
+            output[id][tag] = 'U'
+    if _is_unmerged():
+        for id, tag in _diff_refs(filter='A'):
+            output[id][tag] += 'a'
+        for id, tag in _diff_refs(filter='D'):
+            output[id][tag] += 'd'
+    for id, tag_status in sorted(output.items()):
+        for tag, status in sorted(tag_status.items()):
+            print('{status}\t{id}\t{tag}'.format(
+                status=status, id=id, tag=tag))
+
+
+def _is_unmerged(ref='@{upstream}'):
+    try:
+        (status, fetch_head, stderr) = _git(
+            args=['rev-parse', ref],
+            stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True)
+    except SubprocessError as e:
+        if 'No upstream configured' in e.stderr:
+            return
+        raise
+    (status, base, stderr) = _git(
+        args=['merge-base', 'HEAD', ref],
+        stdout=_subprocess.PIPE, wait=True)
+    return base != fetch_head
+
+class DatabaseCache:
+    def __init__(self):
+        try:
+            from notmuch2 import Database
+            self._notmuch = Database()
+        except ImportError:
+            self._notmuch = None
+        self._known = {}
+
+    def known(self,id):
+        if id in self._known:
+            return self._known[id];
+
+        if self._notmuch:
+            try:
+                _ = self._notmuch.find(id)
+                self._known[id] = True
+            except LookupError:
+                self._known[id] = False
+        else:
+            (_, stdout, stderr) = _spawn(
+                args=['notmuch', 'search', '--output=files', 'id:{0}'.format(id)],
+                stdout=_subprocess.PIPE,
+                wait=True)
+            self._known[id] = stdout != None
+        return self._known[id]
+
+@timed
+def get_status():
+    status = {
+        'deleted': {},
+        'missing': {},
+        }
+    db = DatabaseCache()
+    with PrivateIndex(repo=NOTMUCH_GIT_DIR, prefix=TAG_PREFIX) as index:
+        maybe_deleted = index.diff(filter='D')
+        for id, tags in maybe_deleted.items():
+            if db.known(id):
+                status['deleted'][id] = tags
+            else:
+                status['missing'][id] = tags
+        status['added'] = index.diff(filter='A')
+
+    return status
+
+class PrivateIndex:
+    def __init__(self, repo, prefix):
+        try:
+            _os.makedirs(_os.path.join(repo, 'notmuch'))
+        except FileExistsError:
+            pass
+
+        file_name = 'notmuch/index'
+        self.index_path = _os.path.join(repo, file_name)
+        self.cache_path = _os.path.join(repo, 'notmuch', '{:s}.json'.format(_hex_quote(file_name)))
+
+        self.current_prefix = prefix
+
+        self.prefix = None
+        self.uuid = None
+        self.lastmod = None
+        self.checksum = None
+        self._load_cache_file()
+        self.file_tree = None
+        self._index_tags()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        checksum = _read_index_checksum(self.index_path)
+        (count, uuid, lastmod) = _read_database_lastmod()
+        with open(self.cache_path, "w") as f:
+            _json.dump({'prefix': self.current_prefix, 'uuid': uuid, 'lastmod': lastmod,  'checksum': checksum }, f)
+
+    def _load_cache_file(self):
+        try:
+            with open(self.cache_path) as f:
+                data = _json.load(f)
+                self.prefix = data['prefix']
+                self.uuid = data['uuid']
+                self.lastmod = data['lastmod']
+                self.checksum = data['checksum']
+        except FileNotFoundError:
+            return None
+        except _json.JSONDecodeError:
+            _LOG.error("Error decoding cache")
+            _sys.exit(1)
+
+    @timed
+    def _read_file_tree(self):
+        self.file_tree = {}
+
+        with _git(
+                args=['ls-files', 'tags'],
+                additional_env={'GIT_INDEX_FILE': self.index_path},
+                stdout=_subprocess.PIPE) as git:
+            for file in git.stdout:
+                dir=_os.path.dirname(file)
+                tag=_os.path.basename(file).rstrip()
+                if dir not in self.file_tree:
+                    self.file_tree[dir]=[tag]
+                else:
+                    self.file_tree[dir].append(tag)
+
+
+    def _clear_tags_for_message(self, id):
+        """
+        Clear any existing index entries for message 'id'
+
+        Neither 'id' nor the tags in 'tags' should be encoded/escaped.
+        """
+
+        if self.file_tree == None:
+            self._read_file_tree()
+
+        dir = _id_path(id)
+
+        if dir not in self.file_tree:
+            return
+
+        for file in self.file_tree[dir]:
+            line = '0 0000000000000000000000000000000000000000\t{:s}/{:s}\n'.format(dir,file)
+            yield line
+
+
+    @timed
+    def _index_tags(self):
+        "Write notmuch tags to private git index."
+        prefix = '+{0}'.format(_ENCODED_TAG_PREFIX)
+        current_checksum = _read_index_checksum(self.index_path)
+        if (self.prefix == None or self.prefix != self.current_prefix
+            or self.checksum == None or self.checksum != current_checksum):
+            _git(
+                args=['read-tree', '--empty'],
+                additional_env={'GIT_INDEX_FILE': self.index_path}, wait=True)
+
+        query = _tag_query()
+        clear_tags = False
+        (count,uuid,lastmod) = _read_database_lastmod()
+        if self.prefix == self.current_prefix and self.uuid \
+           and self.uuid == uuid and self.checksum == current_checksum:
+            query = '(and (infix "lastmod:{:d}..")) {:s})'.format(self.lastmod+1, query)
+            clear_tags = True
+        with _spawn(
+                args=['notmuch', 'dump', '--format=batch-tag', '--query=sexp', '--', query],
+                stdout=_subprocess.PIPE) as notmuch:
+            with _git(
+                    args=['update-index', '--index-info'],
+                    stdin=_subprocess.PIPE,
+                    additional_env={'GIT_INDEX_FILE': self.index_path}) as git:
+                for line in notmuch.stdout:
+                    if line.strip().startswith('#'):
+                        continue
+                    (tags_string, id) = [_.strip() for _ in line.split(' -- id:')]
+                    tags = [
+                        _unquote(tag[len(prefix):])
+                        for tag in tags_string.split()
+                        if tag.startswith(prefix)]
+                    id = _xapian_unquote(string=id)
+                    if clear_tags:
+                        for line in self._clear_tags_for_message(id=id):
+                            git.stdin.write(line)
+                    for line in _index_tags_for_message(
+                            id=id, status='A', tags=tags):
+                        git.stdin.write(line)
+
+    @timed
+    def diff(self, filter):
+        """
+        Get an {id: {tag, ...}} dict for a given filter.
+
+        For example, use 'A' to find added tags, and 'D' to find deleted tags.
+        """
+        s = _collections.defaultdict(set)
+        with _git(
+                args=[
+                    'diff-index', '--cached', '--diff-filter', filter,
+                    '--name-only', 'HEAD'],
+                additional_env={'GIT_INDEX_FILE': self.index_path},
+                stdout=_subprocess.PIPE) as p:
+            # Once we drop Python < 3.3, we can use 'yield from' here
+            for id, tag in _unpack_diff_lines(stream=p.stdout):
+                s[id].add(tag)
+        return s
+
+def _read_index_checksum (index_path):
+    """Read the index checksum, as defined by index-format.txt in the git source
+    WARNING: assumes SHA1 repo"""
+    import binascii
+    try:
+        with open(index_path, 'rb') as f:
+            size=_os.path.getsize(index_path)
+            f.seek(size-20);
+            return binascii.hexlify(f.read(20)).decode('ascii')
+    except FileNotFoundError:
+        return None
+
+def _read_database_lastmod():
+    with _spawn(
+            args=['notmuch', 'count', '--lastmod', '*'],
+            stdout=_subprocess.PIPE) as notmuch:
+        (count,uuid,lastmod_str) = notmuch.stdout.readline().split()
+        return (count,uuid,int(lastmod_str))
+
+def _id_path(id):
+    hid=_hex_quote(string=id)
+    from hashlib import blake2b
+
+    if FORMAT_VERSION==0:
+        return 'tags/{hid}'.format(hid=hid)
+    elif FORMAT_VERSION==1:
+        idhash = blake2b(hid.encode('utf8'), digest_size=2).hexdigest()
+        return 'tags/{dir1}/{dir2}/{hid}'.format(
+            hid=hid,
+            dir1=idhash[0:2],dir2=idhash[2:])
+    else:
+        _LOG.error("Unknown format version",FORMAT_VERSION)
+        _sys.exit(1)
+
+def _index_tags_for_message(id, status, tags):
+    """
+    Update the Git index to either create or delete an empty file.
+
+    Neither 'id' nor the tags in 'tags' should be encoded/escaped.
+    """
+    mode = '100644'
+    hash = _EMPTYBLOB
+
+    if status == 'D':
+        mode = '0'
+        hash = '0000000000000000000000000000000000000000'
+
+    for tag in tags:
+        path = '{ipath}/{tag}'.format(ipath=_id_path(id),tag=_hex_quote(string=tag))
+        yield '{mode} {hash}\t{path}\n'.format(mode=mode, hash=hash, path=path)
+
+
+def _diff_refs(filter, a='HEAD', b='@{upstream}'):
+    with _git(
+            args=['diff', '--diff-filter', filter, '--name-only', a, b],
+            stdout=_subprocess.PIPE) as p:
+        # Once we drop Python < 3.3, we can use 'yield from' here
+        for id, tag in _unpack_diff_lines(stream=p.stdout):
+            yield id, tag
+
+
+def _unpack_diff_lines(stream):
+    "Iterate through (id, tag) tuples in a diff stream."
+    for line in stream:
+        match = _TAG_FILE_REGEX[FORMAT_VERSION].match(line.strip())
+        if not match:
+            message = 'non-tag line in diff: {!r}'.format(line.strip())
+            if line.startswith(_TAG_DIRECTORY):
+                raise ValueError(message)
+            _LOG.info(message)
+            continue
+        id = _unquote(match.group('id'))
+        tag = _unquote(match.group('tag'))
+        yield (id, tag)
+
+
+def _help(parser, command=None):
+    """
+    Show help for an notmuch-git command.
+
+    Because some folks prefer:
+
+      $ notmuch-git help COMMAND
+
+    to
+
+      $ notmuch-git COMMAND --help
+    """
+    if command:
+        parser.parse_args([command, '--help'])
+    else:
+        parser.parse_args(['--help'])
+
+def _notmuch_config_get(key):
+    (status, stdout, stderr) = _spawn(
+        args=['notmuch', 'config', 'get', key],
+        stdout=_subprocess.PIPE, wait=True)
+    if status != 0:
+        _LOG.error("failed to run notmuch config")
+        _sys.exit(1)
+    return stdout.rstrip()
+
+def read_format_version():
+    try:
+        (status, stdout, stderr) = _git(
+            args=['cat-file', 'blob', 'master:FORMAT'],
+            stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True)
+    except SubprocessError as e:
+        _LOG.debug("failed to read FORMAT file from git, assuming format version 0")
+        return 0
+
+    return int(stdout)
+
+# based on BaseDirectory.save_data_path from pyxdg (LGPL2+)
+def xdg_data_path(profile):
+    resource = _os.path.join('notmuch',profile,'git')
+    assert not resource.startswith('/')
+    _home = _os.path.expanduser('~')
+    xdg_data_home = _os.environ.get('XDG_DATA_HOME') or \
+        _os.path.join(_home, '.local', 'share')
+    path = _os.path.join(xdg_data_home, resource)
+    return path
+
+if __name__ == '__main__':
+    import argparse
+
+    parser = argparse.ArgumentParser(
+        description=__doc__.strip(),
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument(
+        '-C', '--git-dir', metavar='REPO',
+        help='Git repository to operate on.')
+    parser.add_argument(
+        '-p', '--tag-prefix', metavar='PREFIX',
+        default = None,
+        help='Prefix of tags to operate on.')
+    parser.add_argument(
+        '-N', '--nmbug', action='store_true',
+        help='Set defaults for --tag-prefix and --git-dir for the notmuch bug tracker')
+    parser.add_argument(
+        '-l', '--log-level',
+        choices=['critical', 'error', 'warning', 'info', 'debug'],
+        help='Log verbosity.  Defaults to {!r}.'.format(
+            _logging.getLevelName(_LOG.level).lower()))
+
+    help = _functools.partial(_help, parser=parser)
+    help.__doc__ = _help.__doc__
+    subparsers = parser.add_subparsers(
+        title='commands',
+        description=(
+            'For help on a particular command, run: '
+            "'%(prog)s ... <command> --help'."))
+    for command in [
+            'archive',
+            'checkout',
+            'clone',
+            'commit',
+            'fetch',
+            'help',
+            'init',
+            'log',
+            'merge',
+            'pull',
+            'push',
+            'status',
+            ]:
+        func = locals()[command]
+        doc = _textwrap.dedent(func.__doc__).strip().replace('%', '%%')
+        subparser = subparsers.add_parser(
+            command,
+            help=doc.splitlines()[0],
+            description=doc,
+            formatter_class=argparse.RawDescriptionHelpFormatter)
+        subparser.set_defaults(func=func)
+        if command == 'archive':
+            subparser.add_argument(
+                'treeish', metavar='TREE-ISH', nargs='?', default='HEAD',
+                help=(
+                    'The tree or commit to produce an archive for.  Defaults '
+                    "to 'HEAD'."))
+            subparser.add_argument(
+                'args', metavar='ARG', nargs='*',
+                help=(
+                    "Argument passed through to 'git archive'.  Set anything "
+                    'before <tree-ish>, see git-archive(1) for details.'))
+        elif command == 'checkout':
+            subparser.add_argument(
+                '-f', '--force', action='store_true',
+                help='checkout a large fraction of tags.')
+        elif command == 'clone':
+            subparser.add_argument(
+                'repository',
+                help=(
+                    'The (possibly remote) repository to clone from.  See the '
+                    'URLS section of git-clone(1) for more information on '
+                    'specifying repositories.'))
+        elif command == 'commit':
+            subparser.add_argument(
+                '-f', '--force', action='store_true',
+                help='commit a large fraction of tags.')
+            subparser.add_argument(
+                'message', metavar='MESSAGE', default='', nargs='?',
+                help='Text for the commit message.')
+        elif command == 'fetch':
+            subparser.add_argument(
+                'remote', metavar='REMOTE', nargs='?',
+                help=(
+                    'Override the default configured in branch.<name>.remote '
+                    'to fetch from a particular remote repository (e.g. '
+                    "'origin')."))
+        elif command == 'help':
+            subparser.add_argument(
+                'command', metavar='COMMAND', nargs='?',
+                help='The command to show help for.')
+        elif command == 'init':
+            subparser.add_argument(
+                '--format-version', metavar='VERSION',
+                default = None,
+                help='create format VERSION repository.')
+        elif command == 'log':
+            subparser.add_argument(
+                'args', metavar='ARG', nargs='*',
+                help="Additional argument passed through to 'git log'.")
+        elif command == 'merge':
+            subparser.add_argument(
+                'reference', metavar='REFERENCE', default='@{upstream}',
+                nargs='?',
+                help=(
+                    'Reference, usually other branch heads, to merge into '
+                    "our branch.  Defaults to '@{upstream}'."))
+        elif command == 'pull':
+            subparser.add_argument(
+                'repository', metavar='REPOSITORY', default=None, nargs='?',
+                help=(
+                    'The "remote" repository that is the source of the pull.  '
+                    'This parameter can be either a URL (see the section GIT '
+                    'URLS in git-pull(1)) or the name of a remote (see the '
+                    'section REMOTES in git-pull(1)).'))
+            subparser.add_argument(
+                'refspecs', metavar='REFSPEC', default=None, nargs='*',
+                help=(
+                    'Refspec (usually a branch name) to fetch and merge.  See '
+                    'the <refspec> entry in the OPTIONS section of '
+                    'git-pull(1) for other possibilities.'))
+        elif command == 'push':
+            subparser.add_argument(
+               'repository', metavar='REPOSITORY', default=None, nargs='?',
+                help=(
+                    'The "remote" repository that is the destination of the '
+                    'push.  This parameter can be either a URL (see the '
+                    'section GIT URLS in git-push(1)) or the name of a remote '
+                    '(see the section REMOTES in git-push(1)).'))
+            subparser.add_argument(
+                'refspecs', metavar='REFSPEC', default=None, nargs='*',
+                help=(
+                    'Refspec (usually a branch name) to push.  See '
+                    'the <refspec> entry in the OPTIONS section of '
+                    'git-push(1) for other possibilities.'))
+
+    args = parser.parse_args()
+
+    nmbug_mode = False
+    notmuch_profile = _os.getenv('NOTMUCH_PROFILE','default')
+
+    if args.nmbug or _os.path.basename(__file__) == 'nmbug':
+        nmbug_mode = True
+
+    if args.git_dir:
+        NOTMUCH_GIT_DIR = args.git_dir
+    else:
+        if nmbug_mode:
+            default = _os.path.join('~', '.nmbug')
+        else:
+            default = _notmuch_config_get ('git.path')
+            if default == '':
+                default = xdg_data_path(notmuch_profile)
+
+        NOTMUCH_GIT_DIR = _os.path.expanduser(_os.getenv('NOTMUCH_GIT_DIR', default))
+
+    _NOTMUCH_GIT_DIR = _os.path.join(NOTMUCH_GIT_DIR, '.git')
+    if _os.path.isdir(_NOTMUCH_GIT_DIR):
+        NOTMUCH_GIT_DIR = _NOTMUCH_GIT_DIR
+
+    if args.tag_prefix:
+        TAG_PREFIX = args.tag_prefix
+    else:
+        if nmbug_mode:
+            prefix = 'notmuch::'
+        else:
+            prefix = _notmuch_config_get ('git.tag_prefix')
+
+        TAG_PREFIX =  _os.getenv('NOTMUCH_GIT_PREFIX', prefix)
+
+    _ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,')  # quote ':'
+
+    if args.log_level:
+        level = getattr(_logging, args.log_level.upper())
+        _LOG.setLevel(level)
+
+    # for test suite
+    for var in ['NOTMUCH_GIT_DIR', 'NOTMUCH_GIT_PREFIX', 'NOTMUCH_PROFILE', 'NOTMUCH_CONFIG' ]:
+        _LOG.debug('env {:s} = {:s}'.format(var, _os.getenv(var,'%None%')))
+
+    if _notmuch_config_get('built_with.sexp_queries') != 'true':
+        _LOG.error("notmuch git needs sexp query support")
+        _sys.exit(1)
+
+    if not getattr(args, 'func', None):
+        parser.print_usage()
+        _sys.exit(1)
+
+    # The following two lines are used by the test suite.
+    _LOG.debug('prefix = {:s}'.format(TAG_PREFIX))
+    _LOG.debug('repository = {:s}'.format(NOTMUCH_GIT_DIR))
+
+    if args.func != init:
+        FORMAT_VERSION = read_format_version()
+
+    _LOG.debug('FORMAT_VERSION={:d}'.format(FORMAT_VERSION))
+
+    if args.func == help:
+        arg_names = ['command']
+    else:
+        (arg_names, varargs, varkw) = _inspect.getargs(args.func.__code__)
+    kwargs = {key: getattr(args, key) for key in arg_names if key in args}
+    try:
+        args.func(**kwargs)
+    except SubprocessError as e:
+        if _LOG.level == _logging.DEBUG:
+            raise  # don't mask the traceback
+        _LOG.error(str(e))
+        _sys.exit(1)
index 9fca22db818d05f098703ef8493b9b484f3e0e4a..44297251d02183121685a0cb5bda2cae76cdbea4 100644 (file)
@@ -663,7 +663,7 @@ do_reply (notmuch_database_t *notmuch,
         notmuch_messages_move_to_next (messages)) {
        message = notmuch_messages_get (messages);
 
-       if (mime_node_open (notmuch, message, &params->crypto, &node))
+       if (mime_node_open (notmuch, message, params->duplicate, &params->crypto, &node))
            return 1;
 
        reply = create_reply_message (notmuch, message,
@@ -683,7 +683,7 @@ do_reply (notmuch_database_t *notmuch,
 
            /* Start the original */
            sp->map_key (sp, "original");
-           format_part_sprinter (notmuch, sp, node, true, false);
+           format_part_sprinter (notmuch, sp, node, params->duplicate, true, false);
 
            /* End */
            sp->end (sp);
@@ -715,6 +715,7 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[])
     int opt_index;
     notmuch_show_params_t params = {
        .part = -1,
+       .duplicate = 0,
        .crypto = { .decrypt = NOTMUCH_DECRYPT_AUTO },
     };
     int format = FORMAT_DEFAULT;
@@ -739,6 +740,7 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[])
                                      { "auto", NOTMUCH_DECRYPT_AUTO },
                                      { "true", NOTMUCH_DECRYPT_NOSTASH },
                                      { 0, 0 } } },
+       { .opt_int = &params.duplicate, .name = "duplicate" },
        { .opt_inherit = notmuch_shared_options },
        { }
     };
index 6a54d9c16a0684b28bbc9e98e2c62bed340ba03e..ee9efa7448d78c171ec618d561bb1fa450576864 100644 (file)
 #include "sprinter.h"
 #include "zlib-extra.h"
 
+static const char *
+_get_filename (notmuch_message_t *message, int index)
+{
+    notmuch_filenames_t *filenames = notmuch_message_get_filenames (message);
+    int i = 1;
+
+    for (;
+        notmuch_filenames_valid (filenames);
+        notmuch_filenames_move_to_next (filenames), i++) {
+       if (i >= index)
+           return notmuch_filenames_get (filenames);
+    }
+    return NULL;
+}
+
 static const char *
 _get_tags_as_string (const void *ctx, notmuch_message_t *message)
 {
@@ -658,6 +673,7 @@ format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart
 
 void
 format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
+                     int duplicate,
                      bool output_body,
                      bool include_html)
 {
@@ -669,10 +685,13 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
        sp->begin_map (sp);
        format_message_sprinter (sp, node->envelope_file);
 
+       sp->map_key (sp, "duplicate");
+       sp->integer (sp, duplicate > 0 ? duplicate : 1);
+
        if (output_body) {
            sp->map_key (sp, "body");
            sp->begin_list (sp);
-           format_part_sprinter (ctx, sp, mime_node_child (node, 0), true, include_html);
+           format_part_sprinter (ctx, sp, mime_node_child (node, 0), -1, true, include_html);
            sp->end (sp);
        }
 
@@ -836,7 +855,7 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
     }
 
     for (i = 0; i < node->nchildren; i++)
-       format_part_sprinter (ctx, sp, mime_node_child (node, i), true, include_html);
+       format_part_sprinter (ctx, sp, mime_node_child (node, i), -1, true, include_html);
 
     /* Close content structures */
     for (i = 0; i < nclose; i++)
@@ -850,7 +869,8 @@ format_part_sprinter_entry (const void *ctx, sprinter_t *sp,
                            mime_node_t *node, unused (int indent),
                            const notmuch_show_params_t *params)
 {
-    format_part_sprinter (ctx, sp, node, params->output_body, params->include_html);
+    format_part_sprinter (ctx, sp, node, params->duplicate, params->output_body,
+                         params->include_html);
 
     return NOTMUCH_STATUS_SUCCESS;
 }
@@ -925,7 +945,7 @@ format_part_raw (unused (const void *ctx), unused (sprinter_t *sp),
        char buf[4096];
        notmuch_status_t ret = NOTMUCH_STATUS_FILE_ERROR;
 
-       filename = notmuch_message_get_filename (node->envelope_file);
+       filename = _get_filename (node->envelope_file, params->duplicate);
        if (filename == NULL) {
            fprintf (stderr, "Error: Cannot get message filename.\n");
            goto DONE;
@@ -1004,7 +1024,7 @@ show_message (void *ctx,
        session_key_count_error = notmuch_message_count_properties (message, "session-key",
                                                                    &session_keys);
 
-    status = mime_node_open (local, message, &(params->crypto), &root);
+    status = mime_node_open (local, message, params->duplicate, &(params->crypto), &root);
     if (status)
        goto DONE;
     part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
@@ -1266,6 +1286,7 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
     sprinter_t *sprinter;
     notmuch_show_params_t params = {
        .part = -1,
+       .duplicate = 0,
        .omit_excluded = true,
        .output_body = true,
        .crypto = { .decrypt = NOTMUCH_DECRYPT_AUTO },
@@ -1306,6 +1327,7 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
        { .opt_bool = &params.crypto.verify, .name = "verify" },
        { .opt_bool = &params.output_body, .name = "body" },
        { .opt_bool = &params.include_html, .name = "include-html" },
+       { .opt_int = &params.duplicate, .name = "duplicate" },
        { .opt_inherit = notmuch_shared_options },
        { }
     };
@@ -1324,6 +1346,9 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
     /* specifying a part implies single message display */
     single_message = params.part >= 0;
 
+    /* specifying a duplicate also implies single message display */
+    single_message = single_message || (params.duplicate > 0);
+
     if (format == NOTMUCH_FORMAT_NOT_SPECIFIED) {
        /* if part was requested and format was not specified, use format=raw */
        if (params.part >= 0)
index ac25ae189b78d049d65b70afc09e3dee031f4312..37286b8f491af6428c0d22c3222a01f85039a369 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -211,12 +211,14 @@ typedef struct help_topic {
 } help_topic_t;
 
 static const help_topic_t help_topics[] = {
-    { "search-terms",
-      "Common search term syntax." },
     { "hooks",
       "Hooks that will be run before or after certain commands." },
     { "properties",
       "Message property conventions and documentation." },
+    { "search-terms",
+      "Common infix search term syntax." },
+    { "sexp-queries",
+      "Common s-expression search term syntax." },
 };
 
 static const command_t *
@@ -329,9 +331,7 @@ exec_man (const char *page)
 static int
 _help_for (const char *topic_name)
 {
-    const command_t *command;
-    const help_topic_t *topic;
-    unsigned int i;
+    char *page;
 
     if (! topic_name) {
        printf ("The notmuch mail system.\n\n");
@@ -348,23 +348,9 @@ _help_for (const char *topic_name)
        return EXIT_SUCCESS;
     }
 
-    command = find_command (topic_name);
-    if (command) {
-       char *page = talloc_asprintf (NULL, "notmuch-%s", command->name);
-       exec_man (page);
-    }
+    page = talloc_asprintf (NULL, "notmuch-%s", topic_name);
+    exec_man (page);
 
-    for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
-       topic = &help_topics[i];
-       if (strcmp (topic_name, topic->name) == 0) {
-           char *page = talloc_asprintf (NULL, "notmuch-%s", topic->name);
-           exec_man (page);
-       }
-    }
-
-    fprintf (stderr,
-            "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
-            topic_name);
     return EXIT_FAILURE;
 }
 
@@ -443,11 +429,18 @@ notmuch_command (notmuch_database_t *notmuch,
  * false on errors.
  */
 static bool
-try_external_command (char *argv[])
+try_external_command (const char *config_file_name, char *argv[])
 {
     char *old_argv0 = argv[0];
     bool ret = true;
 
+    if (config_file_name) {
+       if (setenv ("NOTMUCH_CONFIG", config_file_name, 1)) {
+           perror ("setenv");
+           exit (1);
+       }
+    }
+
     argv[0] = talloc_asprintf (NULL, "notmuch-%s", old_argv0);
 
     /*
@@ -507,7 +500,7 @@ main (int argc, char *argv[])
     /* if command->function is NULL, try external command */
     if (! command || ! command->function) {
        /* This won't return if the external command is found. */
-       if (try_external_command (argv + opt_index))
+       if (try_external_command (config_file_name, argv + opt_index))
            fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
                     command_name);
        ret = EXIT_FAILURE;
@@ -538,7 +531,7 @@ main (int argc, char *argv[])
                }
 
                if (status == NOTMUCH_STATUS_NO_CONFIG)
-                   fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
+                   fputs ("Try running 'notmuch setup' to create a configuration.\n", stderr);
 
                return EXIT_FAILURE;
            }
@@ -570,15 +563,10 @@ main (int argc, char *argv[])
                                               NULL,
                                               &notmuch,
                                               &status_string);
-
-       if (status == NOTMUCH_STATUS_NO_CONFIG && ! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) {
-           fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
-           goto DONE;
-       }
        switch (status) {
        case NOTMUCH_STATUS_NO_CONFIG:
            if (! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) {
-               fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
+               fputs ("Try running 'notmuch setup' to create a configuration.\n", stderr);
                goto DONE;
            }
            break;
index 66f0be589cad55364cf49051073707ff519559b6..c92bbd6632d649761b376c1b7fd66c400419e1b6 100755 (executable)
@@ -18,4 +18,67 @@ time_emacs "tag messages" \
    (notmuch-tag msg (list \"+test\"))
    (notmuch-tag msg (list \"-test\"))))"
 
+time_emacs "show warmup" \
+          '(notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}")'
+
+time_emacs "show thread #1" \
+          '(notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}")'
+
+time_emacs "depth bound #1" \
+          '(let ((notmuch-show-depth-limit 0))
+               (notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}"))'
+
+time_emacs "height bound #1" \
+          '(let ((notmuch-show-height-limit -1))
+               (notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}"))'
+
+time_emacs "size bound #1" \
+          '(let ((notmuch-show-max-text-part-size 1))
+               (notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}"))'
+
+time_emacs "show thread #2" \
+          '(notmuch-show "thread:{id:20101208005731.943729010@clark.site}")'
+
+time_emacs "depth bound #2" \
+          '(let ((notmuch-show-depth-limit 0))
+               (notmuch-show "thread:{id:20101208005731.943729010@clark.site}"))'
+
+time_emacs "height bound #2" \
+          '(let ((notmuch-show-height-limit -1))
+               (notmuch-show "thread:{id:20101208005731.943729010@clark.site}"))'
+
+time_emacs "size bound #2" \
+          '(let ((notmuch-show-max-text-part-size 1))
+               (notmuch-show "thread:{id:20101208005731.943729010@clark.site}"))'
+
+time_emacs "show thread #3" \
+          '(notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}")'
+
+time_emacs "depth bound #3" \
+          '(let ((notmuch-show-depth-limit 0))
+               (notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}"))'
+
+time_emacs "height bound #3" \
+          '(let ((notmuch-show-height-limit -1))
+               (notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}"))'
+
+time_emacs "size bound #3" \
+          '(let ((notmuch-show-max-text-part-size 1))
+               (notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}"))'
+
+time_emacs "show thread #4" \
+          '(notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}")'
+
+time_emacs "depth bound #4" \
+          '(let ((notmuch-show-depth-limit 0))
+               (notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}"))'
+
+time_emacs "height bound #4" \
+          '(let ((notmuch-show-height-limit -1))
+               (notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}"))'
+
+time_emacs "size bound #4" \
+          '(let ((notmuch-show-max-text-part-size 1))
+               (notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}"))'
+
 time_done
diff --git a/performance-test/T07-git.sh b/performance-test/T07-git.sh
new file mode 100755 (executable)
index 0000000..11dfec0
--- /dev/null
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+test_description='notmuch-git'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+time_start
+
+time_run 'init' "notmuch git init"
+
+time_run 'commit --force' "notmuch git commit --force"
+time_run 'commit' "notmuch git -l error commit"
+time_run 'commit' "notmuch git -l error commit"
+
+time_run 'checkout' "notmuch git checkout"
+
+time_run 'tag -inbox' "notmuch tag -inbox '*'"
+
+time_run 'checkout --force' "notmuch git checkout --force"
+
+
+
+time_done
index 4897c8142ac73ea47023744cafcfd966985f1a98..58c824a299bdf0e00be9b0092795440e8a03a642 100755 (executable)
@@ -336,6 +336,17 @@ db=Database(config=Database.CONFIG.SEARCH)
 m=db.find('20091117232137.GA7669@griffis1.net')
 to=m.header('To')
 print(to)
+EOF
+          test_expect_equal_file EXPECTED OUTPUT
+
+          test_begin_subtest ".notmuch not ignored in split config ($config)"
+          test_subtest_known_broken
+          generate_message '[dir]=.notmuch/cur' '[subject]="Do not ignore, very important"'
+          NOTMUCH_NEW > OUTPUT
+          notmuch search subject:Do-not-ignore | notmuch_search_sanitize >> OUTPUT
+          cat <<EOF > EXPECTED
+Added 1 new message to the database.
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Do not ignore, very important (inbox unread)
 EOF
           test_expect_equal_file EXPECTED OUTPUT
           ;;
@@ -348,9 +359,6 @@ EOF
           ;;
    esac
 
-   case $config in
-       split|XDG*)
-   esac
    restore_config
    rm -rf home/.local
    rm -rf home/.config
index e1e3b151565d2ebc6016b7f97cf63db24e5f7382..7d5f842d11285f0a7f7e808bcd001fd076f972fa 100755 (executable)
@@ -43,13 +43,13 @@ test_begin_subtest "Permissions on inserted message should be 0600"
 test_expect_equal "600" "$(stat -c %a "$cur_msg_filename")"
 
 test_begin_subtest "Insert message adds default tags"
-output=$(notmuch show --format=json "subject:insert-subject")
+output=$(notmuch show --format=json "subject:insert-subject" | notmuch_json_show_sanitize)
 expected='[[[{
- "id": "'"${gen_msg_id}"'",
+ "id": "XXXXX",
  "crypto": {},
  "match": true,
  "excluded": false,
- "filename": ["'"${cur_msg_filename}"'"],
+ "filename": ["YYYYY"],
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
  "tags": ["inbox","unread"],
index da819190f6e2cd958592e0a2a435fd3affa4ec50..ce6b11b637674f663ff4cfcd44fefc027c1b8bf4 100755 (executable)
@@ -854,6 +854,26 @@ notmuch search date:2009-11-17..2009-11-18 and from:keithp | notmuch_search_sani
 notmuch search --query=sexp  '(and (date 2009-11-17 2009-11-18) (from keithp))' | notmuch_search_sanitize > OUTPUT
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "date query, lower bound only"
+notmuch search date:2009-11-18.. and from:keithp | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  '(and (date 2009-11-18 "") (from keithp))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "date query, upper bound only"
+notmuch search date:..2009-11-17 and from:keithp | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  '(and (date "" 2009-11-17) (from keithp))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "date query, lower bound only, using *"
+notmuch search date:2009-11-18.. and from:keithp | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  '(and (date 2009-11-18 *) (from keithp))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "date query, upper bound only, using *"
+notmuch search date:..2009-11-17 and from:keithp | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  '(and (date * 2009-11-17) (from keithp))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_begin_subtest "date query, illegal nesting 1"
 notmuch search --query=sexp '(to (date))' > OUTPUT 2>&1
 cat <<EOF > EXPECTED
@@ -921,6 +941,26 @@ notmuch search lastmod:$revision..$revision2 | notmuch_search_sanitize > EXPECTE
 notmuch search --query=sexp  "(and (lastmod $revision $revision2))" | notmuch_search_sanitize > OUTPUT
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "lastmod query, lower bound only"
+notmuch search lastmod:$revision.. | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  "(lastmod $revision \"\")" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "lastmod query, upper bound only"
+notmuch search lastmod:..$revision2 | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  "(lastmod \"\" $revision2)" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "lastmod query, lower bound only, using *"
+notmuch search lastmod:$revision.. | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  "(lastmod $revision *)" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "lastmod query, upper bound only, using *"
+notmuch search lastmod:..$revision2 | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp  "(lastmod * $revision2)" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_begin_subtest "lastmod query, illegal nesting 1"
 notmuch search --query=sexp '(to (lastmod))' > OUTPUT 2>&1
 cat <<EOF > EXPECTED
@@ -1115,6 +1155,32 @@ too many arguments to macro
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "Saved Search: bad parameter syntax 5"
+notmuch config set squery.Bad5 '(macro (thing) (tag (rx ,thing)))'
+notmuch search --query=sexp '(Bad5 (1 2))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'rx' expects single atom as argument
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: bad parameter syntax 6"
+notmuch config set squery.Bad6 '(macro (thing) (tag (starts-with ,thing)))'
+notmuch search --query=sexp '(Bad6 (1 2))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'starts-with' expects single atom as argument
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: bad parameter syntax 7"
+notmuch search --query=sexp '(subject (rx ,unknown))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+undefined parameter 'unknown'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
 test_begin_subtest "Saved Search: macro without body"
 notmuch config set squery.Bad3  '(macro (a b))'
 notmuch search --query=sexp '(Bad3)' >OUTPUT 2>&1
@@ -1144,7 +1210,7 @@ notmuch config set squery.Bad6  '(macro (a) (and ,b (subject maildir)))'
 notmuch search --query=sexp '(Bad6 foo)' >OUTPUT 2>&1
 cat <<EOF > EXPECTED
 notmuch search: Syntax error in query
-undefined parameter b
+undefined parameter 'b'
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
@@ -1166,6 +1232,18 @@ notmuch config set squery.TagSubject2  '(macro (tagname subj) (and (tag ,tagname
 notmuch search --query=sexp '(TagSubject2 inbox maildir)' | notmuch_search_sanitize > OUTPUT
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "macro in regex"
+notmuch search tag:inbox and date:2009-11-17 | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.D  '(macro (tagname)  (and (date 2009-11-17) (tag (rx ,tagname))))'
+notmuch search --query=sexp '(D inbo)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "macro in wildcard"
+notmuch search tag:inbox and date:2009-11-17 | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.W  '(macro (tagname)  (and (date 2009-11-17) (tag (starts-with ,tagname))))'
+notmuch search --query=sexp '(W inbo)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_begin_subtest "nested macros (shadowing)"
 notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
 notmuch config set squery.Inner '(macro (x) (subject ,x))'
@@ -1179,10 +1257,24 @@ notmuch config set squery.Outer2  '(macro (x y) (and (tag ,x) (Inner2 ,y)))'
 notmuch search --query=sexp '(Outer2 inbox maildir)' > OUTPUT 2>&1
 cat <<EOF > EXPECTED
 notmuch search: Syntax error in query
-undefined parameter y
+undefined parameter 'y'
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "nested macros (shadowing, regex)"
+notmuch search tag:/inbo/ and subject:/Maildi/ | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.Inner3 '(macro (x) (subject (rx ,x)))'
+notmuch config set squery.Outer3  '(macro (x y) (and (tag (rx ,x)) (Inner3 ,y)))'
+notmuch search --query=sexp '(Outer3 inbo Maildi)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "nested macros (shadowing, wildcard)"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.Inner4 '(macro (x) (subject (starts-with ,x)))'
+notmuch config set squery.Outer4  '(macro (x y) (and (tag (starts-with ,x)) (Inner4 ,y)))'
+notmuch search --query=sexp '(Outer4 inbo maildi)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_begin_subtest "combine macro and user defined header"
 notmuch config set squery.About '(macro (name) (or (subject ,name) (List ,name)))'
 notmuch search subject:notmuch or List:notmuch | notmuch_search_sanitize > EXPECTED
index ec7b1461754cb92bc2863bb66d8c4b30bbc9d94b..4a797f6aad14dfe13c18c8a69d3eac617dcee80e 100755 (executable)
@@ -5,17 +5,17 @@ test_description="--format=json output"
 
 test_begin_subtest "Show message: json"
 add_message "[subject]=\"json-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"json-show-message\""
-output=$(notmuch show --format=json "json-show-message")
-test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
+output=$(notmuch show --format=json "json-show-message" | notmuch_json_show_sanitize)
+test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
 
 # This should be the same output as above.
 test_begin_subtest "Show message: json --body=true"
-output=$(notmuch show --format=json --body=true "json-show-message")
-test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\",  \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
+output=$(notmuch show --format=json --body=true "json-show-message" | notmuch_json_show_sanitize)
+test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\",  \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
 
 test_begin_subtest "Show message: json --body=false"
-output=$(notmuch show --format=json --body=false "json-show-message")
-test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\",  \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}}, []]]]"
+output=$(notmuch show --format=json --body=false "json-show-message" | notmuch_json_show_sanitize)
+test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\",  \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}}, []]]]"
 
 test_begin_subtest "Search message: json"
 add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
@@ -33,8 +33,8 @@ test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
 
 test_begin_subtest "Show message: json, utf-8"
 add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
-output=$(notmuch show --format=json "jsön-show-méssage")
-test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\",  \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]"
+output=$(notmuch show --format=json "jsön-show-méssage" | notmuch_json_show_sanitize)
+test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\",  \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]"
 
 test_begin_subtest "Show message: json, inline attachment filename"
 subject='json-show-inline-attachment-filename'
@@ -49,7 +49,7 @@ output=$(notmuch show --format=json "id:$id")
 filename=$(notmuch search --output=files "id:$id")
 # Get length of README after base64-encoding, minus additional newline.
 attachment_length=$(( $(base64 $NOTMUCH_SRCDIR/test/README | wc -c) - 1 ))
-test_expect_equal_json "$output" "[[[{\"id\": \"$id\",  \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"$filename\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"test_suite@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"content-length\": $attachment_length, \"content-transfer-encoding\": \"base64\", \"content-disposition\": \"inline\", \"filename\": \"README\"}]}]}, []]]]"
+test_expect_equal_json "$output" "[[[{\"id\": \"$id\", \"duplicate\": 1, \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"$filename\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"test_suite@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"content-length\": $attachment_length, \"content-transfer-encoding\": \"base64\", \"content-disposition\": \"inline\", \"filename\": \"README\"}]}]}, []]]]"
 
 test_begin_subtest "Search message: json, utf-8"
 add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
@@ -97,6 +97,7 @@ cat <<EOF > EXPECTED
         [
             {
                 "date_relative": "2001-01-05",
+               "duplicate": 1,
                 "excluded": false,
                 "filename": [
                     "${MAIL_DIR}/copy1",
@@ -132,6 +133,7 @@ cat <<EOF > EXPECTED
         [
             {
                 "date_relative": "2001-01-05",
+               "duplicate": 1,
                 "excluded": false,
                 "filename": "${MAIL_DIR}/copy1",
                 "headers": {
index 0d32560cc1b54d8eaaf5af04bb6859cf7b7e4e50..0be94bd2348fcb68c252a10c469fe7b1a8ef3db7 100755 (executable)
@@ -5,32 +5,32 @@ test_description="--format=sexp output"
 
 test_begin_subtest "Show message: sexp"
 add_message "[subject]=\"sexp-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"sexp-show-message\""
-output=$(notmuch show --format=sexp "sexp-show-message")
-test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))"
+output=$(notmuch show --format=sexp "sexp-show-message" | notmuch_sexp_show_sanitize)
+test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"GENERATED_DATE\")) ())))"
 
 # This should be the same output as above.
 test_begin_subtest "Show message: sexp --body=true"
-output=$(notmuch show --format=sexp --body=true "sexp-show-message")
-test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))"
+output=$(notmuch show --format=sexp --body=true "sexp-show-message" | notmuch_sexp_show_sanitize)
+test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"GENERATED_DATE\")) ())))"
 
 test_begin_subtest "Show message: sexp --body=false"
-output=$(notmuch show --format=sexp --body=false "sexp-show-message")
-test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))"
+output=$(notmuch show --format=sexp --body=false "sexp-show-message" | notmuch_sexp_show_sanitize)
+test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"GENERATED_DATE\")) ())))"
 
 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\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))"
+output=$(notmuch search --format=sexp "sexp-search-message" | notmuch_sexp_search_sanitize)
+test_expect_equal "$output" "((:thread \"XXX\" :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\""
-output=$(notmuch show --format=sexp "jsön-show-méssage")
-test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"jsön-show-méssage\n\")) :crypto () :headers (:Subject \"sexp-show-utf8-body-sübjéct\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))"
+output=$(notmuch show --format=sexp "jsön-show-méssage" | notmuch_sexp_show_sanitize)
+test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"jsön-show-méssage\n\")) :crypto () :headers (:Subject \"sexp-show-utf8-body-sübjéct\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Date \"GENERATED_DATE\")) ())))"
 
 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 \"0000000000000004\" :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\")))"
+output=$(notmuch search --format=sexp "jsön-search-méssage" | notmuch_sexp_search_sanitize)
+test_expect_equal "$output" "((:thread \"XXX\" :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_begin_subtest "Show message: sexp, inline attachment filename"
 subject='sexp-show-inline-attachment-filename'
@@ -45,7 +45,7 @@ output=$(notmuch show --format=sexp "id:$id")
 filename=$(notmuch search --output=files "id:$id")
 # Get length of README after base64-encoding, minus additional newline.
 attachment_length=$(( $(base64 $NOTMUCH_SRCDIR/test/README | wc -c) - 1 ))
-test_expect_equal "$output" "((((:id \"$id\" :match t :excluded nil :filename (\"$filename\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\") :body ((:id 1 :content-type \"multipart/mixed\" :content ((:id 2 :content-type \"text/plain\" :content \"This is a test message with inline attachment with a filename\") (:id 3 :content-type \"application/octet-stream\" :content-disposition \"inline\" :filename \"README\" :content-transfer-encoding \"base64\" :content-length $attachment_length)))) :crypto () :headers (:Subject \"sexp-show-inline-attachment-filename\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"test_suite@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))"
+test_expect_equal "$output" "((((:id \"$id\" :match t :excluded nil :filename (\"$filename\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\") :duplicate 1 :body ((:id 1 :content-type \"multipart/mixed\" :content ((:id 2 :content-type \"text/plain\" :content \"This is a test message with inline attachment with a filename\") (:id 3 :content-type \"application/octet-stream\" :content-disposition \"inline\" :filename \"README\" :content-transfer-encoding \"base64\" :content-length $attachment_length)))) :crypto () :headers (:Subject \"sexp-show-inline-attachment-filename\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"test_suite@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))"
 
 test_begin_subtest "show extra headers"
 add_message "[subject]=\"extra-headers\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[in-reply-to]=\"<parent@notmuch-test-suite>\"" "[body]=\"extra-headers test\""\
@@ -54,10 +54,9 @@ add_message "[subject]=\"extra-headers\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0
        for <test_suite_other@notmuchmail.org>; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)\"" \
 
 notmuch config set show.extra_headers "in-reply-to;received"
-notmuch show --format=sexp --body=false id:${gen_msg_id} | \
-    notmuch_dir_sanitize | sed 's/msg-[0-9]*/MSG/g'> OUTPUT
+notmuch show --format=sexp --body=false id:${gen_msg_id} | notmuch_sexp_show_sanitize > OUTPUT
 cat <<EOF > EXPECTED
-((((:id "MSG@notmuch-test-suite" :match t :excluded nil :filename ("MAIL_DIR/MSG") :timestamp 946728000 :date_relative "2000-01-01" :tags ("inbox" "unread") :crypto () :headers (:Subject "extra-headers" :From "Notmuch Test Suite <test_suite@notmuchmail.org>" :To "Notmuch Test Suite <test_suite@notmuchmail.org>" :Date "Sat, 01 Jan 2000 12:00:00 +0000" :In-Reply-To "<parent@notmuch-test-suite>" :Received "from mail.example.com (mail.example.com [1.1.1.1])\011by mail.notmuchmail.org (some MTA) with ESMTP id 12345678\011for <test_suite_other@notmuchmail.org>; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)")) ())))
+((((:id "XXXXX" :match t :excluded nil :filename ("YYYYY") :timestamp 42 :date_relative "2000-01-01" :tags ("inbox" "unread") :crypto () :headers (:Subject "extra-headers" :From "Notmuch Test Suite <test_suite@notmuchmail.org>" :To "Notmuch Test Suite <test_suite@notmuchmail.org>" :Date "GENERATED_DATE" :In-Reply-To "<parent@notmuch-test-suite>" :Received "from mail.example.com (mail.example.com [1.1.1.1])\011by mail.notmuchmail.org (some MTA) with ESMTP id 12345678\011for <test_suite_other@notmuchmail.org>; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)")) ())))
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
index d3b7f87cc32ea321f03fc520b9eb02fc3e278e90..cfe48ac505dd39eccfc2c9ae12973c02b045b63e 100755 (executable)
@@ -376,18 +376,18 @@ test_begin_subtest "--format=text --part=8, no part, expect error"
 test_expect_success "notmuch show --format=text --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org'"
 
 test_begin_subtest "--format=json --part=0, full message"
-notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT
 cat <<EOF >EXPECTED
-{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "crypto": {}, "match": true, "excluded": false, "filename": ["${MAIL_DIR}/multipart"], "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [
+{"id": "XXXXX", "crypto": {}, "match": true, "excluded": false, "filename": ["YYYYY"], "timestamp": 42, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "GENERATED_DATE"}, "body": [
 {"id": 1, "content-type": "multipart/signed", "content": [
 {"id": 2, "content-type": "multipart/mixed", "content": [
-{"id": 3, "content-type": "message/rfc822", "content-disposition": "inline", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content-disposition": "inline", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "GENERATED_DATE"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
-{"id": 5, "content-type": "text/html", "content-length": 71},
+{"id": 5, "content-type": "text/html", "content-length": "NONZERO"},
 {"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, 
 {"id": 7, "content-type": "text/plain", "content-disposition": "attachment", "filename": "attachment", "content": "This is a text attachment.\n"},
 {"id": 8, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]}, 
-{"id": 9, "content-type": "application/pgp-signature", "content-length": 197}]}]}
+{"id": 9, "content-type": "application/pgp-signature", "content-length": "NONZERO"}]}]}
 EOF
 test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
 
@@ -485,7 +485,7 @@ notmuch show --format=raw 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 test_expect_equal_file "${MAIL_DIR}"/multipart  OUTPUT
 
 test_begin_subtest "--format=raw --part=0, full message"
-notmuch show --format=raw --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+notmuch show --format=raw --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT
 test_expect_equal_file "${MAIL_DIR}"/multipart OUTPUT
 
 test_begin_subtest "--format=raw --part=1, message body"
@@ -727,10 +727,10 @@ notmuch new > /dev/null
 
 cat_expected_head () {
         cat <<EOF
-[[[{"id": "htmlmessage", "match":true, "excluded": false, "date_relative":"2000-01-01",
+[[[{"id": "XXXXX", "match":true, "excluded": false, "date_relative":"2000-01-01",
    "crypto": {},
    "timestamp": 946684800,
-   "filename": ["${MAIL_DIR}/include-html"],
+   "filename": ["YYYYY"],
    "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>"},
@@ -742,8 +742,8 @@ 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": 2, "content-charset": "UTF-8", "content-length": "NONZERO", "content-type": "text/html"},
+  { "id": 3, "content-charset": "ISO-8859-1", "content-length": "NONZERO", "content-type": "text/html"},
   { "id": 4, "content-type": "text/plain", "content": "0.5 equals \\u00bd\\n"}
 ]}]},[]]]]
 EOF
@@ -759,11 +759,11 @@ cat <<EOF >> EXPECTED.withhtml
 EOF
 
 test_begin_subtest "html parts excluded by default"
-notmuch show --format=json id:htmlmessage > OUTPUT
+notmuch show --format=json id:htmlmessage | notmuch_json_show_sanitize > 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
+notmuch show --format=json --include-html id:htmlmessage | notmuch_json_show_sanitize > OUTPUT
 test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED.withhtml)"
 
 test_begin_subtest "indexes mime-type #1"
index e1d50bf90a0a15881d1a7712b1fad813a13009ee..440820286a00169956aca2101c9a12e60aeb5803 100755 (executable)
@@ -64,4 +64,15 @@ for pow in {10..20}; do
     test_expect_success "notmuch show --format=raw subject:$size > /dev/null"
 done
 
+add_email_corpus duplicate
+ID=87r2ecrr6x.fsf@zephyr.silentflame.com
+test_begin_subtest "raw content, duplicate files"
+rm -f OUTPUT.raw
+for dup in {1..5}; do
+    notmuch show --format=raw --duplicate=${dup} --format=raw id:${ID} | md5sum | cut -f1 -d' '  >> OUTPUT.raw
+done
+sort OUTPUT.raw > OUTPUT
+notmuch search --output=files id:${ID} | xargs md5sum | cut -f1 -d ' ' | sort > EXPECTED
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_done
index 4e9984d2ed12f857f1e5de8999e6e7aae868f9cb..207f578810133643491dad884fafb879a8e62312 100755 (executable)
@@ -298,7 +298,8 @@ On Tue, 05 Jan 2010 15:43:56 -0000, ☃ <snowman@example.com> wrote:
 OK"
 
 test_begin_subtest "Reply with RFC 2047-encoded headers (JSON)"
-output=$(echo '{"answer":' && notmuch reply --format=json id:${gen_msg_id} 2>&1 && echo ', "success": "OK"}')
+output=$(echo '{"answer":' && notmuch reply --format=json id:${gen_msg_id} 2>&1 | notmuch_json_show_sanitize \
+        && echo ', "success": "OK"}')
 test_expect_equal_json "$output" '
 {  "answer": {
     "original": {
@@ -312,14 +313,14 @@ test_expect_equal_json "$output" '
         "crypto": {},
         "date_relative": "2010-01-05",
         "excluded": false,
-        "filename": ["'${MAIL_DIR}'/msg-015"],
+        "filename": ["YYYYY"],
         "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}'",
+        "id": "XXXXX",
         "match": false,
         "tags": [
             "inbox",
@@ -352,4 +353,40 @@ On Thu, 16 Jun 2016 22:14:41 -0400, Alice <alice@example.org> wrote:
 > Note the Cc: and cc: headers.
 OK"
 
+add_email_corpus duplicate
+
+ID1=debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15
+
+test_begin_subtest "format json, --duplicate=2, duplicate key"
+output=$(notmuch reply --format=json --duplicate=2 id:${ID1})
+test_json_nodes <<<"$output" "dup:['original']['duplicate']=2"
+
+test_begin_subtest "format json, subject, --duplicate=1"
+output=$(notmuch reply --format=json --duplicate=1 id:${ID1})
+file=$(notmuch search --output=files id:${ID1} | head -n 1)
+subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file)
+test_json_nodes <<<"$output" "subject:['reply-headers']['Subject']=\"Re: $subject\""
+
+test_begin_subtest "format json, subject, --duplicate=2"
+output=$(notmuch reply --format=json --duplicate=2 id:${ID1})
+file=$(notmuch search --output=files id:${ID1} | tail -n 1)
+subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file)
+test_json_nodes <<<"$output" "subject:['reply-headers']['Subject']=\"Re: $subject\""
+
+ID2=87r2geywh9.fsf@tethera.net
+for dup in {1..2}; do
+    test_begin_subtest "format json, body, --duplicate=${dup}"
+    output=$(notmuch reply --format=json --duplicate=${dup} id:${ID2} | \
+            $NOTMUCH_PYTHON -B "$NOTMUCH_SRCDIR"/test/json_check_nodes.py "body:['original']['body'][0]['content']" | \
+            grep '^# body')
+    test_expect_equal "$output" "# body ${dup}"
+done
+
+ID3=87r2ecrr6x.fsf@zephyr.silentflame.com
+for dup in {1..5}; do
+    test_begin_subtest "format json, --duplicate=${dup}, 'duplicate' key"
+    output=$(notmuch reply --format=json --duplicate=${dup} id:${ID3})
+    test_json_nodes <<<"$output" "dup:['original']['duplicate']=${dup}"
+done
+
 test_done
index 0c84b7dd812ed407e17ac88f6dd23f8d8324bd25..35bf70c0df4a155fe498fe2976de6bf5bf7deed9 100755 (executable)
@@ -15,6 +15,15 @@ EOF
     echo "${TOKEN}" > ${2}
 }
 
+create_printenv_hook () {
+    mkdir -p ${HOOK_DIR}
+    cat <<EOF >"${HOOK_DIR}/${1}"
+#!/bin/sh
+printenv "${2}" > "${3}"
+EOF
+    chmod +x "${HOOK_DIR}/${1}"
+}
+
 create_write_hook () {
     local TOKEN="${RANDOM}"
     mkdir -p ${HOOK_DIR}
@@ -53,8 +62,11 @@ add_message
 # create maildir structure for notmuch-insert
 mkdir -p "$MAIL_DIR"/{cur,new,tmp}
 
+ORIG_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
 for config in traditional profile explicit relative XDG split; do
     unset NOTMUCH_PROFILE
+    export NOTMUCH_CONFIG=${ORIG_NOTMUCH_CONFIG}
+    EXPECTED_CONFIG=${NOTMUCH_CONFIG}
     notmuch config set database.hook_dir
     notmuch config set database.path ${MAIL_DIR}
     case $config in
@@ -65,8 +77,10 @@ for config in traditional profile explicit relative XDG split; do
            dir=${HOME}/.config/notmuch/other
            mkdir -p ${dir}
            HOOK_DIR=${dir}/hooks
-           cp ${NOTMUCH_CONFIG} ${dir}/config
+           EXPECTED_CONFIG=${dir}/config
+           cp ${NOTMUCH_CONFIG} ${EXPECTED_CONFIG}
            export NOTMUCH_PROFILE=other
+           unset NOTMUCH_CONFIG
            ;;
        explicit)
            HOOK_DIR=${HOME}/.notmuch-hooks
@@ -200,6 +214,23 @@ EOF
 EOF
     test_expect_equal_file EXPECTED OUTPUT
 
+    test_begin_subtest "NOTMUCH_CONFIG is set"
+    create_printenv_hook "pre-new" NOTMUCH_CONFIG OUTPUT
+    NOTMUCH_NEW
+    cat <<EOF > EXPECTED
+${EXPECTED_CONFIG}
+EOF
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+    test_begin_subtest "NOTMUCH_CONFIG is set by --config"
+    create_printenv_hook "pre-new" NOTMUCH_CONFIG OUTPUT
+    cp "${EXPECTED_CONFIG}" "${EXPECTED_CONFIG}.alternate"
+    notmuch --config "${EXPECTED_CONFIG}.alternate" new
+    cat <<EOF > EXPECTED
+${EXPECTED_CONFIG}.alternate
+EOF
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
+
     rm -rf ${HOOK_DIR}
 done
 test_done
diff --git a/test/T405-external.sh b/test/T405-external.sh
new file mode 100755 (executable)
index 0000000..0e1d964
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+test_description='hooks'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+create_echo_script () {
+    local TOKEN="${RANDOM}"
+    mkdir -p ${BIN_DIR}
+    cat <<EOF >"${BIN_DIR}/${1}"
+#!/bin/sh
+echo "${TOKEN}" > ${3}
+EOF
+    chmod +x "${BIN_DIR}/${1}"
+    echo "${TOKEN}" > ${2}
+}
+
+create_printenv_script () {
+    mkdir -p ${BIN_DIR}
+    cat <<EOF >"${BIN_DIR}/${1}"
+#!/bin/sh
+printenv "${2}" > "${3}"
+EOF
+    chmod +x "${BIN_DIR}/${1}"
+}
+
+# add a message to generate mail dir and database
+add_message
+
+BIN_DIR=`pwd`/bin
+PATH=$BIN_DIR:$PATH
+
+test_begin_subtest "'notmuch foo' runs notmuch-foo"
+rm -rf ${BIN_DIR}
+create_echo_script "notmuch-foo" EXPECTED OUTPUT $HOOK_DIR
+notmuch foo
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+create_printenv_script "notmuch-printenv" NOTMUCH_CONFIG OUTPUT
+
+test_begin_subtest "NOTMUCH_CONFIG is set"
+notmuch printenv
+cat <<EOF > EXPECTED
+${NOTMUCH_CONFIG}
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "NOTMUCH_CONFIG is set by --config"
+cp "${NOTMUCH_CONFIG}" "${NOTMUCH_CONFIG}.alternate"
+cat <<EOF > EXPECTED
+${NOTMUCH_CONFIG}.alternate
+EOF
+notmuch --config "${NOTMUCH_CONFIG}.alternate" printenv
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_done
index 057ad37ee30e1a1e90fef01f82edac906c5101f4..559df8aaea3f15d73fab7e1f5b4d6739de130a20 100755 (executable)
@@ -62,6 +62,17 @@ test_emacs '(let ((notmuch-crypto-process-mime nil))
        (test-visible-output))'
 test_expect_equal_file $EXPECTED/notmuch-show-process-crypto-mime-parts-on OUTPUT
 
+test_begin_subtest "notmuch-search-show-thread returns non-nil on success"
+test_emacs_expect_t  '(notmuch-search "id:20091117203301.GV3165@dottiness.seas.harvard.edu")
+                     (notmuch-test-wait)
+                     (and (notmuch-search-show-thread)
+                          (not (notmuch-show-next-thread)))'
+
+test_begin_subtest "notmuch-search-show-thread returns nil when there are no messages"
+test_emacs_expect_t  '(notmuch-search "id:non-existing-id")
+                     (notmuch-test-wait)
+                     (not (notmuch-search-show-thread))'
+
 test_begin_subtest "notmuch-show: don't elide non-matching messages"
 test_emacs '(let ((notmuch-show-only-matching-messages nil))
        (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
@@ -80,6 +91,62 @@ test_emacs '(let ((notmuch-show-only-matching-messages t))
        (test-visible-output))'
 test_expect_equal_file $EXPECTED/notmuch-show-elide-non-matching-messages-on OUTPUT
 
+test_begin_subtest "Hide bodies of messages by depth"
+test_emacs '(let ((notmuch-show-depth-limit -1))
+       (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-depth OUTPUT
+
+
+test_begin_subtest "Hide bodies of messages by height"
+test_emacs '(let ((notmuch-show-height-limit -1))
+       (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+# folding all messages by height or depth should look the same
+test_expect_equal_file $EXPECTED/notmuch-show-depth OUTPUT
+
+test_begin_subtest "Hide bodies of messages; show only leaves."
+test_emacs '(let ((notmuch-show-height-limit 0))
+       (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-height-0 OUTPUT
+
+test_begin_subtest "Hide bodies of messages (depth > 1)"
+test_emacs '(let ((notmuch-show-depth-limit 1))
+       (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-depth-1 OUTPUT
+
+test_begin_subtest "Hide bodies of messages by size"
+test_emacs '(let ((notmuch-show-max-text-part-size 1))
+       (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-size OUTPUT
+
+test_begin_subtest "Hide bodies of messages by size > 450"
+test_emacs '(let ((notmuch-show-max-text-part-size 450))
+       (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-size-450 OUTPUT
+
 test_begin_subtest "notmuch-show: elide non-matching messages (w/ notmuch-show-toggle-elide-non-matching)"
 test_emacs '(let ((notmuch-show-only-matching-messages nil))
        (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
@@ -209,6 +276,10 @@ test_emacs '(notmuch-show "id:'$gen_msg_id'")
 output=$(head -1 OUTPUT.raw|cut -f1-4 -d' ')
 test_expect_equal "$output" "Notmuch Test Suite <test_suite@notmuchmail.org>"
 
+test_begin_subtest "multipart/alternative hides html by default"
+test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
+           (test-visible-output)'
+test_expect_equal_file $EXPECTED/notmuch-show-multipart-alternative OUTPUT
 
 # switching to the crypto corpus, using gpg from here on:
 add_gnupg_home
@@ -245,4 +316,100 @@ test_emacs "(test-log-error
                (notmuch-show \"$tid\")))"
 test_expect_equal "$(cat MESSAGES)" "COMPLETE"
 
+add_email_corpus attachment
+
+test_begin_subtest "tar not inlined by default"
+test_emacs '(notmuch-show "id:874llc2bkp.fsf@curie.anarc.at")
+       (test-visible-output "OUTPUT")'
+cat <<EOF > EXPECTED
+Antoine Beaupré <anarcat@orangeseeds.org> (2018-03-19) (attachment inbox)
+Subject: Re: bug: "no top level messages" crash on Zen email loops
+To: David Bremner <david@tethera.net>, notmuch@notmuchmail.org
+Date: Mon, 19 Mar 2018 13:56:54 -0400
+
+[ multipart/mixed ]
+[ text/plain ]
+And obviously I forget the frigging attachment.
+[ zendesk-email-loop2.tgz: application/x-gtar-compressed ]
+[ text/plain ]
+
+PS: don't we have a "you forgot to actually attach the damn file" plugin
+when we detect the word "attachment" and there's no attach? :p
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "tar not inlined by default on refresh"
+test_emacs '(notmuch-show "id:874llc2bkp.fsf@curie.anarc.at")
+          (notmuch-show-refresh-view)
+       (test-visible-output "OUTPUT")'
+cat <<EOF > EXPECTED
+Antoine Beaupré <anarcat@orangeseeds.org> (2018-03-19) (attachment inbox)
+Subject: Re: bug: "no top level messages" crash on Zen email loops
+To: David Bremner <david@tethera.net>, notmuch@notmuchmail.org
+Date: Mon, 19 Mar 2018 13:56:54 -0400
+
+[ multipart/mixed ]
+[ text/plain ]
+And obviously I forget the frigging attachment.
+[ zendesk-email-loop2.tgz: application/x-gtar-compressed ]
+[ text/plain ]
+
+PS: don't we have a "you forgot to actually attach the damn file" plugin
+when we detect the word "attachment" and there's no attach? :p
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+add_email_corpus duplicate
+
+ID3=87r2ecrr6x.fsf@zephyr.silentflame.com
+test_begin_subtest "duplicate=3, subject"
+test_emacs "(notmuch-show \"id:${ID3}\")
+          (notmuch-show-choose-duplicate 3)
+          (test-visible-output \"OUTPUT\")"
+output=$(grep "Subject:" OUTPUT)
+file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1)
+subject=$(grep '^Subject:' $file)
+test_expect_equal "$output" "$subject"
+
+FILE3=$(notmuch search --output=files --duplicate=3 "id:${ID3}")
+test_begin_subtest "duplicate=3, stash"
+test_emacs_expect_t \
+       "(notmuch-show \"id:${ID3}\")
+        (notmuch-show-choose-duplicate 3)
+        (notmuch-show-stash-filename)
+        (notmuch-test-expect-equal (list (car kill-ring)) (list \"${FILE3}\"))"
+
+test_begin_subtest "duplicate=0"
+test_emacs "(test-log-error
+             (notmuch-show \"id:${ID3}\")
+             (notmuch-show-choose-duplicate 0))"
+cat <<EOF > EXPECTED
+(error Duplicate 0 out of range [1,5])
+EOF
+test_expect_equal_file EXPECTED MESSAGES
+
+test_begin_subtest "duplicate=1000"
+test_emacs "(test-log-error
+             (notmuch-show \"id:${ID3}\")
+             (notmuch-show-choose-duplicate 1000))"
+cat <<EOF > EXPECTED
+(error Duplicate 1000 out of range [1,5])
+EOF
+test_expect_equal_file EXPECTED MESSAGES
+test_begin_subtest "duplicate=4"
+test_emacs "(notmuch-show \"id:${ID3}\")
+          (notmuch-show-choose-duplicate 4)
+          (test-visible-output \"OUTPUT\")"
+test_expect_equal_file_nonempty $EXPECTED/notmuch-show-duplicate-4 OUTPUT
+
+FILE4=$(notmuch search --output=files --duplicate=4 "id:${ID3}")
+test_begin_subtest "duplicate=4, raw"
+test_emacs "(notmuch-show \"id:${ID3}\")
+          (notmuch-show-choose-duplicate 4)
+          (notmuch-show-view-raw-message)
+          (test-visible-output \"OUTPUT\")"
+subject4=$(grep '^Subject:' $FILE4)
+subject=$(grep '^Subject:' OUTPUT)
+test_expect_equal "$subject4" "$subject"
+
 test_done
diff --git a/test/T453-emacs-reply.sh b/test/T453-emacs-reply.sh
new file mode 100755 (executable)
index 0000000..0a27d06
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+
+test_description="emacs reply"
+. $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+EXPECTED=$NOTMUCH_SRCDIR/test/emacs-reply.expected-output
+
+test_require_emacs
+
+add_email_corpus attachment
+
+test_begin_subtest "tar not inlined by default"
+test_emacs '(notmuch-mua-new-reply "id:874llc2bkp.fsf@curie.anarc.at")
+       (test-visible-output "OUTPUT.raw")'
+cat <<EOF > EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Antoine Beaupré <anarcat@orangeseeds.org>
+Subject: Re: bug: "no top level messages" crash on Zen email loops
+In-Reply-To: <874llc2bkp.fsf@curie.anarc.at>
+Fcc: MAIL_DIR/sent
+--text follows this line--
+Antoine Beaupré <anarcat@orangeseeds.org> writes:
+
+> And obviously I forget the frigging attachment.
+>
+>
+> PS: don't we have a "you forgot to actually attach the damn file" plugin
+> when we detect the word "attachment" and there's no attach? :p
+EOF
+notmuch_dir_sanitize < OUTPUT.raw > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+add_email_corpus duplicate
+
+ID2=87r2geywh9.fsf@tethera.net
+for dup in {1..2}; do
+    test_begin_subtest "body, duplicate=${dup}"
+    test_emacs "(notmuch-show \"id:${ID2}\")
+          (notmuch-test-wait)
+          (notmuch-show-choose-duplicate $dup)
+          (notmuch-test-wait)
+          (notmuch-show-reply)
+          (test-visible-output \"OUTPUT.raw\")"
+    output=$(grep '^> # body' OUTPUT.raw)
+    test_expect_equal "$output" "> # body ${dup}"
+done
+
+ID3=87r2ecrr6x.fsf@zephyr.silentflame.com
+test_begin_subtest "duplicate=3, subject"
+test_emacs "(notmuch-show \"id:${ID3}\")
+          (notmuch-test-wait)
+          (notmuch-show-choose-duplicate 3)
+          (notmuch-test-wait)
+          (notmuch-show-reply)
+          (test-visible-output \"OUTPUT\")"
+output=$(sed -n 's/^Subject: //p' OUTPUT)
+file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1)
+subject=$(sed -n 's/^Subject: //p' $file)
+test_expect_equal "$output" "Re: $subject"
+
+test_begin_subtest "duplicate=4"
+test_emacs "(notmuch-show \"id:${ID3}\")
+          (notmuch-show-choose-duplicate 4)
+          (notmuch-test-wait)
+          (notmuch-show-reply)
+          (test-visible-output \"OUTPUT.raw\")"
+notmuch_dir_sanitize < OUTPUT.raw > OUTPUT
+test_expect_equal_file_nonempty $EXPECTED/notmuch-reply-duplicate-4 OUTPUT
+
+test_done
diff --git a/test/T454-emacs-dont-reply-names.sh b/test/T454-emacs-dont-reply-names.sh
new file mode 100755 (executable)
index 0000000..3a77017
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+
+test_description="message-dont-reply-to-names in emacs replies"
+. $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+EXPECTED=$NOTMUCH_SRCDIR/test/emacs-show.expected-output
+
+test_require_emacs
+
+add_email_corpus default
+
+test_begin_subtest "regular expression"
+test_emacs '(let ((message-dont-reply-to-names "notmuchmail\\|noreply\\|harvard"))
+             (notmuch-mua-new-reply
+               "id:20091117203301.GV3165@dottiness.seas.harvard.edu" nil t)
+             (test-visible-output "OUTPUT-FULL.raw"))'
+
+notmuch_dir_sanitize < OUTPUT-FULL.raw > OUTPUT-FULL
+head -6 OUTPUT-FULL > OUTPUT
+
+cat <<EOF > EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Mikhail Gusarov <dottedmag@dottedmag.net>
+Subject: Re: [notmuch] Working with Maildir storage?
+In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu>
+Fcc: MAIL_DIR/sent
+--text follows this line--
+EOF
+
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "predicate"
+test_emacs '(let ((message-dont-reply-to-names
+                  (lambda (m) (string-prefix-p "Mikhail" m))))
+             (notmuch-mua-new-reply
+               "id:20091117203301.GV3165@dottiness.seas.harvard.edu" nil t)
+             (test-visible-output "OUTPUT-FULL-PRED.raw"))'
+
+notmuch_dir_sanitize < OUTPUT-FULL-PRED.raw > OUTPUT-FULL-PRED
+head -7 OUTPUT-FULL-PRED > OUTPUT-PRED
+
+cat <<EOF > EXPECTED-PRED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Cc: notmuch@notmuchmail.org
+Subject: Re: [notmuch] Working with Maildir storage?
+In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu>
+Fcc: MAIL_DIR/sent
+--text follows this line--
+EOF
+
+test_expect_equal_file EXPECTED-PRED OUTPUT-PRED
+
+test_begin_subtest "nil value"
+test_emacs '(let ((message-dont-reply-to-names nil))
+             (notmuch-mua-new-reply
+               "id:20091117203301.GV3165@dottiness.seas.harvard.edu" nil t)
+             (test-visible-output "OUTPUT-FULL-NIL.raw"))'
+
+notmuch_dir_sanitize < OUTPUT-FULL-NIL.raw > OUTPUT-FULL-NIL
+head -7 OUTPUT-FULL-NIL > OUTPUT-NIL
+
+cat <<EOF > EXPECTED-NIL
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Lars Kellogg-Stedman <lars@seas.harvard.edu>, Mikhail Gusarov <dottedmag@dottedmag.net>
+Cc: notmuch@notmuchmail.org
+Subject: Re: [notmuch] Working with Maildir storage?
+In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu>
+Fcc: MAIL_DIR/sent
+--text follows this line--
+EOF
+
+test_expect_equal_file EXPECTED-NIL OUTPUT-NIL
+
+test_done
index 0f23b418ecfa1bd8da04676f9d327a14b2e7fe5e..3a1c449ef2b4350dd7d08397b5213ea1efb526fc 100755 (executable)
@@ -200,4 +200,26 @@ test_emacs '(test-log-error
                (notmuch-tree "*")))'
 test_expect_equal "$(cat MESSAGES)" "COMPLETE"
 
+add_email_corpus duplicate
+
+ID3=87r2ecrr6x.fsf@zephyr.silentflame.com
+test_begin_subtest "duplicate=3, subject"
+test_emacs "(notmuch-tree \"id:${ID3}\")
+          (notmuch-test-wait)
+          (notmuch-tree-show-message t)
+          (notmuch-show-choose-duplicate 3)
+          (test-visible-output \"OUTPUT\")"
+output=$(grep "Subject:" OUTPUT)
+file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1)
+subject=$(grep '^Subject:' $file)
+test_expect_equal "$output" "$subject"
+
+test_begin_subtest "duplicate=4"
+test_emacs "(notmuch-show \"id:${ID3}\")
+          (notmuch-test-wait)
+          (notmuch-tree-show-message t)
+          (notmuch-show-choose-duplicate 4)
+          (test-visible-output \"OUTPUT\")"
+test_expect_equal_file_nonempty $NOTMUCH_SRCDIR/test/emacs-show.expected-output/notmuch-show-duplicate-4 OUTPUT
+
 test_done
index e7bc1439c50dc66632d5b3f905c55a5a59f9335b..a3ff85fd01a3ddb24570011132d86397f92d9a27 100755 (executable)
@@ -57,4 +57,29 @@ test_emacs '(test-log-error
                (notmuch-unthreaded "*")))'
 test_expect_equal "$(cat MESSAGES)" "COMPLETE"
 
+add_email_corpus duplicate
+
+ID3=87r2ecrr6x.fsf@zephyr.silentflame.com
+test_begin_subtest "duplicate=3, subject"
+test_emacs "(let ((notmuch-tree-show-out t))
+             (notmuch-unthreaded \"id:${ID3}\")
+             (notmuch-test-wait)
+             (notmuch-tree-show-message nil)
+             (notmuch-show-choose-duplicate 3)
+             (test-visible-output \"OUTPUT\"))"
+output=$(grep "Subject:" OUTPUT)
+file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1)
+subject=$(grep '^Subject:' $file)
+test_expect_equal "$output" "$subject"
+
+test_begin_subtest "duplicate=4"
+test_emacs "(let ((notmuch-tree-show-out t))
+             (notmuch-unthreaded \"id:${ID3}\")
+             (notmuch-test-wait)
+             (notmuch-tree-show-message nil)
+             (notmuch-show-choose-duplicate 4)
+             (test-visible-output \"OUTPUT\"))"
+test_expect_equal_file_nonempty $NOTMUCH_SRCDIR/test/emacs-show.expected-output/notmuch-show-duplicate-4 OUTPUT
+
+
 test_done
index 12bde6c78f8912140fd2ec0fe276a0247caaf8c2..c7b73a6d4a2bc2a9f892732f49052e1d97052183 100755 (executable)
@@ -45,4 +45,40 @@ if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then
 
 fi
 
+add_email_corpus duplicate
+
+ID1=debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15
+
+test_begin_subtest "format json, --duplicate=2, duplicate key"
+output=$(notmuch show --format=json --duplicate=2 id:${ID1})
+test_json_nodes <<<"$output" "dup:['duplicate']=2"
+
+test_begin_subtest "format json, subject, --duplicate=1"
+output=$(notmuch show --format=json --duplicate=1 id:${ID1})
+file=$(notmuch search --output=files id:${ID1} | head -n 1)
+subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file)
+test_json_nodes <<<"$output" "subject:['headers']['Subject']=\"$subject\""
+
+test_begin_subtest "format json, subject, --duplicate=2"
+output=$(notmuch show --format=json --duplicate=2 id:${ID1})
+file=$(notmuch search --output=files id:${ID1} | tail -n 1)
+subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file)
+test_json_nodes <<<"$output" "subject:['headers']['Subject']=\"$subject\""
+
+ID2=87r2geywh9.fsf@tethera.net
+for dup in {1..2}; do
+    test_begin_subtest "format json, body, --duplicate=${dup}"
+    output=$(notmuch show --format=json --duplicate=${dup} id:${ID2} | \
+            $NOTMUCH_PYTHON -B "$NOTMUCH_SRCDIR"/test/json_check_nodes.py "body:['body'][0]['content']" | \
+            grep '^# body')
+    test_expect_equal "$output" "# body ${dup}"
+done
+
+ID3=87r2ecrr6x.fsf@zephyr.silentflame.com
+for dup in {1..5}; do
+    test_begin_subtest "format json, --duplicate=${dup}, 'duplicate' key"
+    output=$(notmuch show --format=json --duplicate=${dup} id:${ID3})
+    test_json_nodes <<<"$output" "dup:['duplicate']=${dup}"
+done
+
 test_done
index 1f4482cbe3d30cadf43ca362aa7f4ea946e22d70..a2901ff6404f8ba1d4794bb9bf46c787be4312e0 100755 (executable)
@@ -16,7 +16,11 @@ int main (int argc, char** argv)
 {
     notmuch_database_t *db;
     notmuch_status_t stat;
-    stat = notmuch_database_open (NULL, 0, 0);
+    char* msg = NULL;
+    stat = notmuch_database_open_with_config (NULL,
+                                             NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                             "", NULL, &db, &msg);
+    if (msg) fputs (msg, stderr);
 }
 EOF
 cat <<'EOF' >EXPECTED
@@ -34,7 +38,11 @@ int main (int argc, char** argv)
 {
     notmuch_database_t *db;
     notmuch_status_t stat;
-    stat = notmuch_database_open ("./nonexistent/foo", 0, 0);
+    char *msg = NULL;
+    stat = notmuch_database_open_with_config ("./nonexistent/foo",
+                                            NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                            "", NULL, &db, &msg);
+    if (msg) fputs (msg, stderr);
 }
 EOF
 cat <<'EOF' >EXPECTED
@@ -52,7 +60,10 @@ int main (int argc, char** argv)
 {
     notmuch_database_t *db;
     notmuch_status_t stat;
-    stat = notmuch_database_create ("./nonexistent/foo", &db);
+    char *msg = NULL;
+
+    stat = notmuch_database_create_with_config ("./nonexistent/foo", "", NULL, &db, &msg);
+    if (msg) fputs (msg, stderr);
 }
 EOF
 cat <<'EOF' >EXPECTED
@@ -70,7 +81,11 @@ int main (int argc, char** argv)
 {
     notmuch_database_t *db;
     notmuch_status_t stat;
-    stat = notmuch_database_open (argv[1], 0, 0);
+    char* msg = NULL;
+    stat = notmuch_database_open_with_config (argv[1],
+                                             NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                             "", NULL, &db, &msg);
+    if (msg) fputs (msg, stderr);
 }
 EOF
 cat <<'EOF' >EXPECTED
@@ -87,7 +102,10 @@ test_C <<'EOF'
 int main (int argc, char** argv)
 {
     notmuch_status_t stat;
-    stat = notmuch_database_create (NULL, NULL);
+    char *msg;
+
+    stat = notmuch_database_create_with_config (NULL, "", NULL, NULL, &msg);
+    if (msg) fputs (msg, stderr);
 }
 EOF
 cat <<'EOF' >EXPECTED
@@ -105,7 +123,10 @@ int main (int argc, char** argv)
 {
     notmuch_database_t *db;
     notmuch_status_t stat;
-    stat = notmuch_database_create (argv[1], &db);
+    char *msg;
+
+    stat = notmuch_database_create_with_config (argv[1], "", NULL, &db, &msg);
+    if (msg) fputs (msg, stderr);
 }
 EOF
 cat <<'EOF' >EXPECTED
@@ -123,7 +144,11 @@ int main (int argc, char** argv)
 {
    notmuch_database_t *db;
    notmuch_status_t stat;
-   stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, &db);
+   char* msg = NULL;
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                            "", NULL, &db, &msg);
+   if (msg) fputs (msg, stderr);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d\n", stat);
    }
@@ -148,7 +173,9 @@ int main (int argc, char** argv)
 {
    notmuch_database_t *db;
    notmuch_status_t stat;
-   stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db);
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            "", NULL, &db, NULL);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d\n", stat);
    }
@@ -206,7 +233,9 @@ int main (int argc, char** argv)
    char *msg = NULL;
    int fd;
 
-   stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg);
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            NULL, NULL, &db, &msg);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
      exit (1);
@@ -245,24 +274,6 @@ EOF
 test_expect_equal_file EXPECTED OUTPUT.clean
 restore_database
 
-backup_database
-test_begin_subtest "Xapian exception getting tags"
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH}
-   {
-       notmuch_tags_t *tags = NULL;
-       tags = notmuch_database_get_all_tags (db);
-       stat = (tags == NULL);
-   }
-EOF
-sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean
-cat <<'EOF' >EXPECTED
-== stdout ==
-== stderr ==
-A Xapian exception occurred getting tags
-EOF
-test_expect_equal_file EXPECTED OUTPUT.clean
-restore_database
-
 backup_database
 test_begin_subtest "Xapian exception creating directory"
 cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH}
index 2314efd220a2bd79575809094b8fffffa0122848..fedfc9ed15730a7c8dfbd5b5463032daa4bda8df 100755 (executable)
@@ -17,7 +17,9 @@ int main (int argc, char** argv)
    notmuch_status_t stat = NOTMUCH_STATUS_SUCCESS;
    char *msg = NULL;
 
-   stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg);
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            NULL, NULL, &db, &msg);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
      exit (1);
@@ -241,14 +243,14 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
         const char *path = talloc_asprintf(db, "%s/01:2,", argv[1]);
         EXPECT0(notmuch_database_close (db));
         stat = notmuch_database_index_file (db, path, NULL, &msg);
-        printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        printf ("%d\n", stat == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
 == stdout ==
 1
 == stderr ==
-A Xapian exception occurred finding message: Database has been closed.
+Cannot write to a closed database.
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
@@ -356,14 +358,14 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         EXPECT0(notmuch_database_close (db));
         stat = notmuch_database_set_config (db, "foo", "bar");
-        printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        printf("%d\n", stat == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
 == stdout ==
 1
 == stderr ==
-Error: A Xapian exception occurred setting metadata: Database has been closed
+Cannot write to a closed database.
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
index ebd7fcb2d358d3be118d11c40362facbed9547e2..4711fcdfaa1f192c1d8d8c431558a23524fc5b94 100755 (executable)
@@ -18,7 +18,9 @@ int main (int argc, char** argv)
    notmuch_status_t stat = NOTMUCH_STATUS_SUCCESS;
    char *msg = NULL;
 
-   stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg);
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            NULL, NULL, &db, &msg);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
      exit (1);
@@ -77,14 +79,14 @@ test_begin_subtest "delete directory document for a closed db"
 cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         stat = notmuch_directory_delete (dir);
-        printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        printf ("%d\n", stat == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
 == stdout ==
 1
 == stderr ==
-A Xapian exception occurred deleting directory entry: Database has been closed.
+Cannot write to a closed database.
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 restore_database
@@ -95,32 +97,14 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         time_t stamp = notmuch_directory_get_mtime (dir);
         stat = notmuch_directory_set_mtime (dir, stamp);
-        printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        printf ("%d\n", stat == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
 == stdout ==
 1
 == stderr ==
-A Xapian exception occurred setting directory mtime: Database has been closed.
-EOF
-test_expect_equal_file EXPECTED OUTPUT
-restore_database
-
-backup_database
-test_begin_subtest "get/set mtime of directory for a closed db"
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
-    {
-        time_t stamp = notmuch_directory_get_mtime (dir);
-        stat = notmuch_directory_set_mtime (dir, stamp);
-        printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
-    }
-EOF
-cat <<EOF > EXPECTED
-== stdout ==
-1
-== stderr ==
-A Xapian exception occurred setting directory mtime: Database has been closed.
+Cannot write to a closed database.
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 restore_database
index ff1d49849bc571775d69a9af3d46ddaf89d23cb7..53a63bf61ac29611d390a355e8ddf35f2b3b8f4d 100755 (executable)
@@ -17,7 +17,9 @@ int main (int argc, char** argv)
    notmuch_status_t stat;
    char *msg = NULL;
 
-   stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg);
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            NULL, NULL, &db, &msg);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
      exit (1);
diff --git a/test/T565-lib-tags.sh b/test/T565-lib-tags.sh
new file mode 100755 (executable)
index 0000000..2a59f8d
--- /dev/null
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+test_description="API tests for tags"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "building database"
+test_expect_success "NOTMUCH_NEW"
+
+cat <<EOF > c_head
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <talloc.h>
+#include <notmuch.h>
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   char *path;
+   char *msg = NULL;
+   int fd;
+
+   stat = notmuch_database_open_with_config (argv[1],
+                                             NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                             NULL, NULL, &db, &msg);
+   if (stat != NOTMUCH_STATUS_SUCCESS) {
+     fprintf (stderr, "error opening database\n%s\n%s\n", notmuch_status_to_string (stat), msg ? msg : "");
+     exit (1);
+   }
+EOF
+cat <<'EOF' > c_tail
+   if (stat) {
+       const char *stat_str = notmuch_database_status_string (db);
+       if (stat_str)
+          fputs (stat_str, stderr);
+    }
+
+}
+EOF
+
+POSTLIST_PATH=(${MAIL_DIR}/.notmuch/xapian/postlist.*)
+
+backup_database
+test_begin_subtest "Xapian exception getting tags"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH}
+   {
+      notmuch_tags_t *tags = NULL;
+      fd = open(argv[2],O_WRONLY|O_TRUNC);
+      if (fd < 0) {
+         fprintf (stderr, "error opening %s\n", argv[1]);
+         exit (1);
+       }
+       tags = notmuch_database_get_all_tags (db);
+       stat = (tags == NULL);
+   }
+EOF
+sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+A Xapian exception occurred getting tags
+EOF
+test_expect_equal_file EXPECTED OUTPUT.clean
+restore_database
+
+test_begin_subtest "NULL tags are not valid"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+   {
+       notmuch_bool_t valid = TRUE;
+       valid = notmuch_tags_valid (NULL);
+       fprintf(stdout, "valid = %d\n", valid);
+   }
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+valid = 0
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
index 8b61d18215af0698803c0c0b4b4fab0762a6f3e4..511d56ca694e5c131d4d5815ad43d3e80ee5fbb1 100755 (executable)
@@ -27,9 +27,11 @@ int main (int argc, char** argv)
    notmuch_status_t stat;
    char *msg = NULL;
    notmuch_message_t *message = NULL;
-   const char *id = "1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+   const char *id = "87pr7gqidx.fsf@yoom.home.cworth.org";
 
-   stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg);
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            NULL, NULL, &db, &msg);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
      exit (1);
@@ -82,7 +84,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
 EOF
 cat <<EOF > EXPECTED
 == stdout ==
-1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+87pr7gqidx.fsf@yoom.home.cworth.org
 1
 == stderr ==
 EOF
@@ -154,7 +156,7 @@ cat c_head0 - c_tail <<'EOF' | test_C ${MAIL_DIR}
 EOF
 cat <<EOF > EXPECTED
 == stdout ==
-MAIL_DIR/01:2,
+MAIL_DIR/cur/40:2,
 SUCCESS
 == stderr ==
 EOF
@@ -229,7 +231,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         notmuch_status_t status;
         status = notmuch_message_add_tag (message, "boom");
-        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
@@ -245,7 +247,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         notmuch_status_t status;
         status = notmuch_message_remove_tag (message, "boom");
-        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
@@ -305,12 +307,126 @@ cat <<EOF > EXPECTED
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "_notmuch_message_add_term catches exceptions"
+cat c_head0 - c_tail <<'EOF' | test_private_C ${MAIL_DIR}
+    {
+       notmuch_private_status_t status;
+       /* This relies on Xapian throwing an exception for adding empty terms */
+       status = _notmuch_message_add_term (message, "body", "");
+       printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS );
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "_notmuch_message_remove_term catches exceptions"
+cat c_head0 - c_tail <<'EOF' | test_private_C ${MAIL_DIR}
+    {
+       notmuch_private_status_t status;
+       /* Xapian throws the same exception for empty and non-existent terms;
+        * error string varies between Xapian versions. */
+       status = _notmuch_message_remove_term (message, "tag", "nonexistent");
+       printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_SUCCESS );
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "_notmuch_message_add_filename on closed db"
+cat c_head - c_tail <<'EOF' | test_private_C ${MAIL_DIR}
+    {
+       notmuch_private_status_t status;
+       status = _notmuch_message_add_filename (message, "some-filename");
+       printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "_notmuch_message_remove_filename on closed db"
+cat c_head - c_tail <<'EOF' | test_private_C ${MAIL_DIR}
+    {
+       notmuch_private_status_t status;
+       status = _notmuch_message_remove_filename (message, "some-filename");
+       printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle converting tags to maildir flags with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+       notmuch_status_t status;
+       status = notmuch_message_tags_to_maildir_flags (message);
+       printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+POSTLIST_PATH=(${MAIL_DIR}/.notmuch/xapian/postlist.*)
+test_begin_subtest "Handle converting tags to maildir flags with corrupted db"
+backup_database
+cat c_head0 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH}
+    {
+        notmuch_status_t status;
+
+        status = notmuch_message_add_tag (message, "draft");
+        if (status) exit(1);
+
+        int fd = open(argv[2],O_WRONLY|O_TRUNC);
+        if (fd < 0) {
+            fprintf (stderr, "error opening %s\n", argv[1]);
+            exit (1);
+        }
+
+        status = notmuch_message_tags_to_maildir_flags (message);
+        printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+restore_database
+notmuch new
+notmuch tag -draft id:87pr7gqidx.fsf@yoom.home.cworth.org
+test_expect_equal_file EXPECTED OUTPUT
+
 test_begin_subtest "Handle removing all tags with closed db"
 cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         notmuch_status_t status;
         status = notmuch_message_remove_all_tags (message);
-        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
@@ -326,7 +442,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         notmuch_status_t status;
         status = notmuch_message_freeze (message);
-        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_SUCCESS);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
@@ -342,7 +458,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
     {
         notmuch_status_t status;
         status = notmuch_message_thaw (message);
-        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_CLOSED_DATABASE);
     }
 EOF
 cat <<EOF > EXPECTED
index b45836cdd989ca6ac25bc58bc15f7a13fa38dd63..b4c24ca23b049b4ec8e06a67d28e9d7fb1bae407 100755 (executable)
@@ -36,7 +36,9 @@ int main (int argc, char** argv)
    notmuch_query_t *query = NULL;
    const char *id = "${THREAD}";
 
-   stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg);
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            NULL, NULL, &db, &msg);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
      exit (1);
index a59e7c980fc97c7c0fb4658cfe616da63d5a9e15..e1cc684de35c32dbb67aed25e03a151668b59628 100755 (executable)
@@ -19,7 +19,12 @@ int main (int argc, char** argv)
 
    unsigned long rev;
 
-   stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, &db);
+   char* msg = NULL;
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                            "", NULL, &db, &msg);
+   if (msg) fputs (msg, stderr);
+
    if (stat)
        fputs ("open failed\n", stderr);
    revision = notmuch_database_get_revision (db, &uuid);
index 4ec854748532d105cd5e1a71256536b066b89cf3..2685f3b5f5b04c62b274c46e51475f6526587a20 100755 (executable)
@@ -23,8 +23,12 @@ int main (int argc, char** argv)
    notmuch_message_t *message = NULL;
    const char *val;
    notmuch_status_t stat;
+   char* msg = NULL;
 
-   EXPECT0(notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db));
+   EXPECT0(notmuch_database_open_with_config (argv[1],
+                                             NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                             "", NULL, &db, &msg));
+   if (msg) fputs (msg, stderr);
    EXPECT0(notmuch_database_find_message(db, "4EFC743A.3060609@april.org", &message));
    if (message == NULL) {
        fprintf (stderr, "unable to find message");
index 8f4c380f110891f402038927d5c5e37c872b3dc5..99cc70103d07105f8cee6acbe19df2f50612f461 100755 (executable)
@@ -40,15 +40,25 @@ main (int argc, char **argv)
 
     if (child == 0) {
        notmuch_database_t *db2;
+       char* msg = NULL;
 
        sleep (1);
-       EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, &db2));
+
+        EXPECT0(notmuch_database_open_with_config (argv[1],
+                                                  NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                                  "", NULL, &db2, &msg));
+        if (msg) fputs (msg, stderr);
+
        taggit (db2, "child");
        EXPECT0 (notmuch_database_close (db2));
     } else {
        notmuch_database_t *db;
+       char* msg = NULL;
 
-       EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, &db));
+        EXPECT0(notmuch_database_open_with_config (argv[1],
+                                                  NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                                  "", NULL, &db, &msg));
+        if (msg) fputs (msg, stderr);
        taggit (db, "parent");
        sleep (2);
        EXPECT0 (notmuch_database_close (db));
index 636b20c71eda0ab214f2c7b9da98e10d43398b6c..2c3fa7356134664e37fe04f8148656f495c9ae42 100755 (executable)
@@ -23,14 +23,22 @@ main (int argc, char **argv)
     notmuch_query_t *query;
     notmuch_tags_t *tags;
     int i;
+    char* msg = NULL;
 
-    EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_ONLY, &ro_db));
+    EXPECT0(notmuch_database_open_with_config (argv[1],
+                                              NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                              "", NULL, &ro_db, &msg));
+    if (msg) fputs (msg, stderr);
     assert(ro_db);
 
     EXPECT0 (notmuch_database_find_message (ro_db, "${first_id}", &ro_message));
     assert(ro_message);
 
-    EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, &rw_db));
+    EXPECT0(notmuch_database_open_with_config (argv[1],
+                                              NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                              "", NULL, &rw_db, &msg));
+    if (msg) fputs (msg, stderr);
+
     query = notmuch_query_create(rw_db, "");
     EXPECT0 (notmuch_query_search_messages (query, &messages));
 
index 3d94d4df9d243a32871c04ad44d82ac3f282f3b5..e5afeaa253ae8bf323167dcd8992ee0e48770c3f 100755 (executable)
@@ -23,7 +23,13 @@ int main (int argc, char** argv)
 {
    notmuch_database_t *db;
    notmuch_status_t stat;
-   stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, &db);
+   char* msg = NULL;
+
+   stat = notmuch_database_open_with_config (argv[1],
+                                            NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                            "", NULL, &db, &msg);
+   if (msg) fputs (msg, stderr);
+
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d\n", stat);
      exit (1);
diff --git a/test/T850-git.sh b/test/T850-git.sh
new file mode 100755 (executable)
index 0000000..55cec78
--- /dev/null
@@ -0,0 +1,396 @@
+#!/usr/bin/env bash
+test_description='"notmuch git" to save and restore tags'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+if [ $NOTMUCH_HAVE_SFSEXP -ne 1 ]; then
+    printf "Skipping due to missing sfsexp library\n"
+    test_done
+fi
+
+# be very careful using backup_database / restore_database in this
+# file, as they fool the cache invalidation checks in notmuch-git.
+
+add_email_corpus
+
+git config --global user.email notmuch@example.org
+git config --global user.name  "Notmuch Test Suite"
+
+test_begin_subtest "init"
+test_expect_success "notmuch git -p '' -C remote.git init"
+
+test_begin_subtest "init (git.path)"
+notmuch config set git.path configured.git
+notmuch git init
+notmuch config set git.path
+output=$(git -C configured.git rev-parse --is-bare-repository)
+test_expect_equal "$output" "true"
+
+test_begin_subtest "clone"
+test_expect_success "notmuch git -p '' -C tags.git clone remote.git"
+
+test_begin_subtest "initial commit needs force"
+test_expect_code 1 "notmuch git -C tags.git commit"
+
+test_begin_subtest "committing new prefix requires force"
+notmuch git -C force-prefix.git init
+notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit"
+notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+
+test_begin_subtest "committing new prefix works with force"
+notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit --force
+git -C force-prefix.git ls-tree -r --name-only HEAD |  notmuch_git_sanitize | xargs dirname | sort -u > OUTPUT
+notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+cat <<EOF>EXPECTED
+20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "checkout new prefix requires force"
+test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout"
+
+test_begin_subtest "checkout new prefix works with force"
+notmuch dump > BEFORE
+notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout --force
+notmuch dump --include=tags id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
+notmuch restore < BEFORE
+cat <<EOF > EXPECTED
++inbox +new-prefix%3a%3afoo +signed +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "commit"
+notmuch git -C tags.git commit --force
+git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | xargs dirname | sort -u > OUTPUT
+notmuch search --output=messages '*' | sed s/^id:// | sort > EXPECTED
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "commit --force succeeds"
+notmuch git -C force.git init
+test_expect_success "notmuch git -C force.git commit --force"
+
+test_begin_subtest "changing git.safe_fraction succeeds"
+notmuch config set git.safe_fraction 1
+notmuch git -C force2.git init
+test_expect_success "notmuch git -C force2.git commit"
+notmuch config set git.safe_fraction
+
+test_begin_subtest "commit, with quoted tag"
+notmuch git -C clone2.git clone tags.git
+git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > BEFORE
+notmuch tag '+"quoted tag"' '*'
+notmuch git -C clone2.git commit
+notmuch tag '-"quoted tag"' '*'
+git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > AFTER
+test_expect_equal_file_nonempty BEFORE AFTER
+
+test_begin_subtest "commit (incremental)"
+notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git commit
+git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | \
+    grep 20091117190054 | sort > OUTPUT
+echo "--------------------------------------------------" >> OUTPUT
+notmuch tag -test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git commit
+git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | \
+    grep 20091117190054 | sort >> OUTPUT
+cat <<EOF > EXPECTED
+20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+20091117190054.GU3165@dottiness.seas.harvard.edu/test
+20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+--------------------------------------------------
+20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "commit (change prefix)"
+notmuch tag +test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p 'test::' commit --force
+git -C tags.git ls-tree -r --name-only HEAD |
+    grep 20091117190054 | notmuch_git_sanitize | sort > OUTPUT
+echo "--------------------------------------------------" >> OUTPUT
+notmuch tag -test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git commit --force
+git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | \
+    grep 20091117190054 | sort >> OUTPUT
+cat <<EOF > EXPECTED
+20091117190054.GU3165@dottiness.seas.harvard.edu/one
+--------------------------------------------------
+20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+backup_database
+test_begin_subtest "large checkout needs --force"
+notmuch tag -inbox '*'
+test_expect_code 1 "notmuch git -C tags.git checkout"
+restore_database
+
+test_begin_subtest "checkout (git.safe_fraction)"
+notmuch git -C force3.git clone tags.git
+notmuch dump > BEFORE
+notmuch tag -inbox '*'
+notmuch config set git.safe_fraction 1
+notmuch git -C force3.git checkout
+notmuch config set git.safe_fraction
+notmuch dump > AFTER
+test_expect_equal_file_nonempty BEFORE AFTER
+
+test_begin_subtest "checkout"
+notmuch dump > BEFORE
+notmuch tag -inbox '*'
+notmuch git -C tags.git checkout --force
+notmuch dump > AFTER
+test_expect_equal_file_nonempty BEFORE AFTER
+
+test_begin_subtest "archive"
+notmuch git -C tags.git archive | tar tf - | \
+    grep 20091117190054.GU3165@dottiness.seas.harvard.edu | notmuch_git_sanitize | sort > OUTPUT
+cat <<EOF > EXPECTED
+20091117190054.GU3165@dottiness.seas.harvard.edu/
+20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+EOF
+notmuch git -C tags.git checkout
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "status"
+notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git status > OUTPUT
+cat <<EOF > EXPECTED
+A      20091117190054.GU3165@dottiness.seas.harvard.edu        test
+EOF
+notmuch git -C tags.git checkout
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "status (global config argument)"
+cp notmuch-config notmuch-config.new
+notmuch --config=notmuch-config.new config set git.path tags.git
+notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch --config=./notmuch-config.new git status > OUTPUT
+cat <<EOF > EXPECTED
+A      20091117190054.GU3165@dottiness.seas.harvard.edu        test
+EOF
+notmuch --config=notmuch-config.new git checkout
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "fetch"
+notmuch tag +test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C remote.git commit --force
+notmuch tag -test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git fetch
+notmuch git -C tags.git status > OUTPUT
+cat <<EOF > EXPECTED
+ a     20091117190054.GU3165@dottiness.seas.harvard.edu        test2
+EOF
+notmuch git -C tags.git checkout
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "merge"
+notmuch git -C tags.git merge
+notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
+cat <<EOF > EXPECTED
++inbox +signed +test2 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "push"
+notmuch tag +test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git commit
+notmuch tag -test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git push
+notmuch git -C remote.git checkout
+notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
+cat <<EOF > EXPECTED
++inbox +signed +test2 +test3 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "environment passed through when run as 'notmuch git'"
+env NOTMUCH_GIT_DIR=foo NOTMUCH_GIT_PREFIX=bar NOTMUCH_PROFILE=default notmuch git -C tags.git -p '' -ldebug status |& \
+    grep '^env ' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+env NOTMUCH_GIT_DIR = foo
+env NOTMUCH_GIT_PREFIX = bar
+env NOTMUCH_PROFILE = default
+env NOTMUCH_CONFIG = CWD/notmuch-config
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--nmbug argument sets defaults"
+notmuch git -ldebug --nmbug status |& grep '^\(prefix\|repository\)' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = notmuch::
+repository = CWD/home/.nmbug
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "invoke as nmbug sets defaults"
+"$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^\(prefix\|repository\)' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = notmuch::
+repository = CWD/home/.nmbug
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR works when invoked as nmbug"
+NOTMUCH_GIT_DIR=`pwd`/foo "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/foo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR works when invoked as 'notmuch git'"
+NOTMUCH_GIT_DIR=`pwd`/remote.git notmuch git -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/remote.git
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR overrides config when invoked as 'nmbug'"
+notmuch config set git.path `pwd`/bar
+NOTMUCH_GIT_DIR=`pwd`/remote.git  "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+cat <<EOF > EXPECTED
+repository = CWD/remote.git
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR overrides config when invoked as 'notmuch git'"
+notmuch config set git.path `pwd`/bar
+NOTMUCH_GIT_DIR=`pwd`/remote.git notmuch git -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+cat <<EOF > EXPECTED
+repository = CWD/remote.git
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as 'nmbug'"
+NOTMUCH_GIT_PREFIX=env:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = env::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as nmbug"
+NOTMUCH_GIT_PREFIX=foo:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = foo::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX overrides config when invoked as 'nmbug'"
+notmuch config set git.tag_prefix config::
+NOTMUCH_GIT_PREFIX=env:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+cat <<EOF > EXPECTED
+prefix = env::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX overrides config when invoked as 'notmuch git'"
+notmuch config set git.tag_prefix config::
+NOTMUCH_GIT_PREFIX=env:: notmuch git -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+cat <<EOF > EXPECTED
+prefix = env::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "init, xdg default location"
+repo=home/.local/share/notmuch/default/git
+notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/$repo
+CWD/$repo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "init, xdg default location, with profile"
+repo=home/.local/share/notmuch/work/git
+NOTMUCH_PROFILE=work notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/$repo
+CWD/$repo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "init, configured location"
+repo=configured-tags
+notmuch config set git.path `pwd`/$repo
+notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/$repo
+CWD/$repo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "configured tag prefix"
+notmuch config set git.tag_prefix test::
+notmuch git -ldebug status |& grep '^prefix' > OUTPUT
+notmuch config set git.tag_prefix
+cat <<EOF > EXPECTED
+prefix = test::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "default version is 1"
+notmuch git -l debug -C default-version.git init
+output=$(git -C default-version.git cat-file blob HEAD:FORMAT)
+test_expect_equal "${output}" 1
+
+test_begin_subtest "illegal version"
+test_expect_code 1 "notmuch git -l debug -C default-version.git init --format-version=42"
+
+hash=("" "8d/c3/") # for use in synthetic repo contents.
+for ver in {0..1}; do
+    test_begin_subtest "init version=${ver}"
+    notmuch git -C version-${ver}.git  -p "test${ver}::" init --format-version=${ver}
+    output=$(git -C version-${ver}.git ls-tree -r --name-only HEAD)
+    expected=("" "FORMAT")
+    test_expect_equal "${output}" "${expected[${ver}]}"
+
+    test_begin_subtest "initial commit version=${ver}"
+    notmuch tag "+test${ver}::a" "+test${ver}::b" id:20091117190054.GU3165@dottiness.seas.harvard.edu
+    notmuch git -C version-${ver}.git -p "test${ver}::" commit --force
+    git -C version-${ver}.git ls-tree -r --name-only HEAD | grep -v FORMAT > OUTPUT
+cat <<EOF > EXPECTED
+tags/${hash[${ver}]}20091117190054.GU3165@dottiness.seas.harvard.edu/a
+tags/${hash[${ver}]}20091117190054.GU3165@dottiness.seas.harvard.edu/b
+EOF
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+    test_begin_subtest "second commit repo=${ver}"
+    notmuch tag "+test${ver}::c" "+test${ver}::d" id:20091117190054.GU3165@dottiness.seas.harvard.edu
+    notmuch git -C version-${ver}.git  -p "test${ver}::" commit --force
+    git -C version-${ver}.git ls-tree -r --name-only HEAD | grep -v FORMAT > OUTPUT
+cat <<EOF > EXPECTED
+tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/a
+tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/b
+tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/c
+tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/d
+EOF
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+    test_begin_subtest "checkout repo=${ver} "
+    notmuch dump > BEFORE
+    notmuch tag -test::${ver}::a '*'
+    notmuch git -C version-${ver}.git  -p "test${ver}::" checkout --force
+    notmuch dump > AFTER
+    test_expect_equal_file_nonempty BEFORE AFTER
+done
+
+test_done
diff --git a/test/corpora/attachment/x-gtar-compressed.eml b/test/corpora/attachment/x-gtar-compressed.eml
new file mode 100644 (file)
index 0000000..258a74d
--- /dev/null
@@ -0,0 +1,136 @@
+Return-path: <anarcat@orangeseeds.org>
+Envelope-to: david@tethera.net
+Delivery-date: Mon, 19 Mar 2018 13:56:54 -0400
+Received: from marcos.anarc.at ([206.248.172.91])
+       by fethera.tethera.net with esmtp (Exim 4.89)
+       (envelope-from <anarcat@orangeseeds.org>)
+       id 1exz1i-0002aa-If
+       for david@tethera.net; Mon, 19 Mar 2018 13:56:54 -0400
+Received: from [127.0.0.1] (localhost [127.0.0.1])     (Authenticated sender: anarcat) with ESMTPSA id 718A610E04F
+From: =?utf-8?Q?Antoine_Beaupr=C3=A9?= <anarcat@orangeseeds.org>
+To: David Bremner <david@tethera.net>, notmuch@notmuchmail.org
+Subject: Re: bug: "no top level messages" crash on Zen email loops
+In-Reply-To: <87a7v42bv9.fsf@curie.anarc.at>
+References: <87d10042pu.fsf@curie.anarc.at> <87woy8vx7i.fsf@tesseract.cs.unb.ca> <87a7v42bv9.fsf@curie.anarc.at>
+Date: Mon, 19 Mar 2018 13:56:54 -0400
+Message-ID: <874llc2bkp.fsf@curie.anarc.at>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+X-Spam_score: 0.0
+X-Spam_score_int: 0
+X-Spam_bar: /
+
+--=-=-=
+Content-Type: text/plain
+
+And obviously I forget the frigging attachment.
+
+
+--=-=-=
+Content-Type: application/x-gtar-compressed
+Content-Disposition: attachment; filename=zendesk-email-loop2.tgz
+Content-Transfer-Encoding: base64
+
+H4sIAJX1r1oAA+xbaVPbyNbO19Gv6CF1K0mBrM2SLYOY8QoGbAzegKlbVFtq28KyJNSSF269//09
+Lclm35LJTebOiARbvZw+a5+npcPIDh08EMwoED58r0uEKyeK7FPKqfc+V9cHSZFVJZeTlWz2gyjJ
+OUn9gNTvxtGdK6IhDhD6gF0cmDh8dtxr/X/Ra3Rrf0mVpaym5FQlc6orWl7P5hQpmxdzmi4qSj4D
+Y2xSkLdq7XeuwQysPWt/WZIVOba/LGqalJPA/jktJ35A4neR+MH1N7f/KQmjwOVbOBwX0A6NfN8L
+wt8Tr8iY3nSXO+OPA3tku9jhO14BpXrYTIb8Ht9mcMhViGPPSECsF0edEpPAMKuAhoE3RVNo9mhm
+1Y0+O56JnbFHQ/QHbAIZEX6kf3/hfhkskZUssLwzugUDh/biC5rb4RhV241OC9kWkrJFVZHEqqiK
+3C9DL0A7z7Czu406EdlCkoIa4ASyKOXhV0HOFZQc4sWsKKLP1UrnC+ig7eMpXx4Tc0ICvkcCantu
+AbHWIqWYUttFSiabkdBnoKLCXF7Of0Ge+1DEFakjMiNOAa1u2yEOI1pATW8LUdMLiMFLGRUF5Dqy
+QaeGmhFRSGhIjVLxvNq+VMWtymG9cdmu7zWrlS3ul/iuVzyqV7b2O42jy9pxs3N5dNy/LMOX02K7
+kzQ3qu12ca+6dVruVS7rzctKs91nrRXuFxyFnkNw4BpjPEXru0tQoEkM10OzRGojlhMY3wvI0rFp
+WGCmwUtiIUXVECWm51oUgcV8MM8IBoEoiobAYIkunle6hkDbD33EIkMSgA6yGd+ztIyNLSlzY9El
+Ze6JPr/c/YekyxlJkzIS7GqSpKe+NF287EVt5kY1WS7HbpStfbUbaffc6IFkXhRKTwv1bM99eUSR
+yfM5Au8boc5ReyZl5EQK0/bHJEDFarmyzxerbVnV+PZ+EbxT1QT4jwZ2SL+wyU0PmY5N3BCZJAjt
+oQ0CktjxwN2ItYq9F23wnAbL2Ww+q0lFuShKr6qwTyxQYfZWhSKoUCpkRbTJ0gT63O2U36HC9U6S
+ib9Z3hRDjD7aVZ4n8MzmUiqWq2VZlHM5XftamRT9JZluiGsROkm4mHvBRHrOF0AMSc5AalbFrxVH
+L5WknMzScFX9k8SpgAMVXhvL1UDUAtqzwyM8QO0k7zydgE6J7yzjrPKW0Wxc8Z4Ez0vENQhs3iPC
+1yuQ/HQlp5801AtFv1QxzmsDSckOzeHgUsplAQANTT2HZXOg582cpOetS+oHUfj7HWvtcnWXv+X2
+DsH7o55cSoVslWVLkcH9pbRc9qml2tHgipiw+Z57UbDSTKqQVfiiMYaNmBAXGhIX4xr2lNzmLykj
+cmXPDSH8+c7SB7NNIye0fRyEAnZCErg4hGnbHBp4kWvhYGls8LxhXE6BDBu1Zl+XMAjzWFOypGU3
+YL45xgEloRGFQz5/u2aAXQpbC191Tc+CXayAcrAzQXIpQv4BXVLfcynhmcUDsBWY1nG4uA/kn9ph
+yKKG5Sp+RFwSgOdZMLmBbYcEBXSRKAwl99CRgpQlHzOwAO1BGgY+bIvP5lVFy+miLMt5GJlO5Zmb
+8kXTBPFDvg6LgVxDmYgcS7h82x6BhqIAFIdmhrSNsBFQzNMxhj12G5lGAJlxQSwh/dxGlnHHiqCX
+a8NyqRAuwm1EV11AJzQAjYt6HlC4zrQ/NpRNOdob+eOjA9ITx7WS2r7qdxZd3PA79SNrTq8ts7G8
+tptddWTAjLFhsTBkG0ohiF0y9Arwb5p6vG0VbJdf99DUm5hd+TTRF8zUSiHzjPXNymQkNRljzzgy
+G/Ogrzfz+pV442Fts0lmvd5hq7dcqBPVy17NDhzizlsnh61KLWfnG/kDf7h57Cw2ezUXq6Q66YkL
+pxYe1azRebBnXWiT3nH7qnR81VfzWAoWVDmzRq4iir090eotdGtfEMpWcZSjEC65aqPeqmXdUjXn
+BV4w3FzOTFKpn8tndauy31ke2KcTTdZPz9STzRPjK03Hge1GkEQcQDGTyM/cteP/ihkPi8L1/szO
+iaMDoX5Fg9HeZrNfypYOdHvzpNs5L0/y43arNtwTivsDyTpzrIDcXC33RktJGeXV3l4Qnkxvjqwr
+eeDX/H1ltF+JDs91dzZoNoh/le+PvRHBfpdG+PrcOVnsh2fyvjsrDejgeKYuOuc3m1HPOSlNatHs
+tGtP8y0tS2oz/8yiE41cn12U+62uV5meluoGBGm5UazybQaXC0hc3cPm7yypDXvFzIDkiMyZUTlo
+j/dPawOwqoToFH6FgQFnA2y0XMc5HjQkd+HOLYWqejAVvBPDKEhSDjzi0C4vDiriYrNF1F6xfXI4
+PfcOAnMZHXswRGcU6hOzM9mviMOLRrEgiTDp4sKNzsODidf30hZf7FuNKh4W4SrkoaHIF5f62UxJ
+GhiZmyKuRxd86JxUJVO1nIYNk3UYelI9syrHchQq5ym1DolOrhs6vZ3cbk+Pa9VieU1f79jl42sq
+t7JLOgi7XbN6YVbPvSXNFxnfMEKdNaIbrB8oZvekeDNeQG5mlC77l+3LXA8w/Em62jC4yRaj8ogf
+d1kL25p76UHIut1hEwhaduA41EM8Kh9Vi80/xUD9iXM17VnnEzyw5ovh9awYNI+/0UAzudI6mam8
+M5y/0WSMzn2bobebjE2+bzP0fpMxIk/YDL3FZKtse0TcETvmZ3NanuM4nn9/Kn+AFlgOFXwHkPV7
+Uvx15EGe5v3AdkM8cAjHffzIoxYcNylBbJdCS4Zp4l0N4YE3g9axTZFjuwTxHz9y3L79AONtcVxn
+jN0Jm4kYimX7HDZD5pX3UWOGASZkYjelH3oJccK2dnZIxQbnQjNohfVhy0IumSPbBapTQENwpI+n
+QK8NMQD0+mNAF/HCc2yHW/G3KV6ioe0anIXGxPGHkXOPApxEmISWZ0ZTUFLSCGuPw9CnBUGADpq5
+RbUZZIC9GN+eDzDHSjhO1mcHbQpRyCSNKAK5YkluxcQOhSNeQNixLplicBTNbByzMCcDpq/p3dVT
+FHmHAWFsCsTlIyqkyJIKoJQM6hgcKCKezxiCdSHUrZWC/MCzIjNkCvkUkHSPwG6iEtC4SyH9xlyw
+FQ2OIdeQ4GkMW7HjxEQswshSMILJcl4QWwywKCS3kNGKXQVWAj9K9dQnyPG8CWNrjoOYG7YgW5yZ
+BnCk58zWPpYAZTqGxZ3lrxxXYrcBGcFMugVORVb+wzjjuIodL4kmrjf/ba3jCSE+bFnYnCBvGLMO
+H/ECicZpLDaIY4NnRnCSdsEIiMYPfVK9rNTAlMCMArFBgiE2ya+oZ1P7JesAKUaeAqKBaQ8sP/KQ
+ZQP8DxNfX6kTwioK7/lR7AAG95ILrI2vy2pWBs9nDwqgD3zYSsVgJksmA6n7gsRxiVkvCWz4Shbs
+C+APAuHi4lFqoLXOMqgax6TpGRxbArTIpBqAT9jDoW3CSYVJNPQcx5tnwDSe+ymMDZOQWWkTJ+gd
++ZhSOE5bv6HSMgmSJGa2WNCsBs3B9QwODht8EjJWzDWz+CAKkyC3wbwuSdwcnImEqQ+m5CHuwTUz
+6JQpC0INGIh3kHX/nUhL1XsHSgpwmBkLM1lwPFCIsJp0GTBSbLepxeLGerZdGgYQYLB5ULalxDG3
+WiXhLNnVgNERe/oBQkIMASrlvCdVlEHNRHmJkKBptj2z6ZSQp9z61ySLvHRBCK13V/gC9ifBzGbe
+wJ51JLG1hequmQETrq4/kgMyz07I/zb+vFQ1DqfOt2WqnV8rx+XOeauKGC3U6paO6mW0wQtCXykL
+QqVTQWfsSSs7WqOYos3sgx1BqDY30AazPBh+Pje4eWauZLxgJHROhQWjJrHp6dcEoKdzM1Zobexy
+O6yHfRBs7XII7UwhmGNf4tlz4pmhVDbuCr2BUrTPOm7lX4uvVGIFGNwGEmJ6NFw6SQZezzAp3WB9
+CIWJL1joP/EtQgNwM9CWCQ6JfQoqXn3bjgf8H6MoxCSBZyFhemfgWZCvWCNbYm5bDJBIovivX+0p
+c0XswhEKTljg/AAZt8GfrcQU4nbMx45lz27nr3sl0V+g7Rgh8LDLjsZwtJfy/mIbwhe0McRT21kW
+0KejyLQtjPYMDtRrkU9bPRJYsPlsFQMbO1sUdM6Dg9rDdCK1b0AwSWaUQDwvKHzMxtd2qpX7/CRD
+0MeByn5gzNuAjcGtoM2OAPRSyv7uY5izI/i70PFmrGNwr6GdN2Mdg3sN7bwZ6wDCYmvvgPMGZMgU
+9wzw2QC2HdbvAq6IH0EHG7v3xhrc7egdAe9mUKqi96MlSKNP46Wn0dIT3K+zpsG9Ap1eFOxe8jW4
+F8gkEnfeiMHA6V9FYW/EYJDcX0BhqQneBsRA7U9CsZTIPTy2Mwhgq3oAytKBgMwSUt+OzSCgDe49
+2OwlX7BBV29x58eQi1n3PrK7Ded3Y7uHLBrcG2He60yDX71MInHTF9FiqljYGb4ZLaZY0eBeQIsr
+n3kbZIRj3Kug8Y2QkUGv10Djk5DxsYsZ3LvwI6z5rCkBW74TjQK1xKoP8ajBfQ0ifRKPQuC8H5Ey
+y8bII06kO9QHO63y86c0hdfi69PuPZgJWAXG7u4IDKLEiCVBWyzB2yGZUhPSSfxtBZBSLEfNMQgV
+I7nYBdOXShu3sMCyqe/AjrIsuJ5LNnZT2MBowZ7psyE4VtrG21aCgBsJPZvMi8ms3RgvgGdO7tGM
+AmfjhX0pNjZEydNBLyREY4h5l6iLp/eBJeMj1r7BgYexaYnuk99fCd7h3PCjK2P+HtdT9V9y5lRS
+JFXKaWpeVHN6Ts3ns+LXF4C9Vv8lyuK6/ksVFVb/lctK/9R//Teu/8X6L12vFtVvqP9SxIKq//D6
+LzmTf6r+q9g/2kpqwMQfXgP2TKXWMxUoL3c/qNTS3lOplVdK+do3VGo9NvjjMqNnSp+e7XlQqaX8
+lyu13l4FFGuwokga7PG6JGez361S6xkVvr1S6+3lZ3H1WbVUrspSuVSVf85KrfeJU6xVS1oizute
+/rer1Hpv+dQ3VGq9tyhsXam1Ppq6XvyYYx4fJf3fvltRFs4puvWUUvKS9NcqypLyP2VRFhlqN165
+oUuji173Cotdunm1lLs9s3W4SZvjC8+cuYqiTIY31R9QzRPOWsv6jVKea5KrRjK1j+3lqV1tFA9J
+ZVqN5EpjJLWpqmQ3xVZrNuiMgqgf9AcXE+fgZqlqp5V6y86OvcPNrq2NbkTBHmud5cnZnhb6tQa5
+yC7qWX3veulH0ol85kndWaT1W+cVkndzFbl0IOTnLW0zur7YrPhFnHOspi17/n5vNrW0vcNydCQO
+m3mnP7Mq0+yPLcr6uc1Ir498tecfDoOlM+rIzdzNYE4qVLFMs3rWyGbn3qQhBAv3Iqv7ujA/rxw0
+G4PhsJRfnp1dYKl0IB6eDh2hW7v2Wvnzi9Z5kJPxZlXWpe5o8+ZakrsHttroyz133mgdLIr+5pk+
+PiG6WLxpErNUuTruurp6bZcP3Vnvahx2hKMeaU310qA5znfnZbWvXI+F69pVb/KeoqzTxrGVHVxv
+/hxFWWzWdNY8xAvXGbv7IZ87qJzW6XM1WHbZPpbdw8XeeLQq8SkuOmU6j/hnirweFW49KgJiRF4v
+3Ko07bl3SDRvOVxWrvDZk0VAjNSDKiAYNArq067bwycVcrg3lTsdr14syPL3Kel63rw/T0nX39Xg
+jwrCNEl5oSDsJRjxkxSEvVaF8KAmgXvwWjXGvpKyFf/ZjhT/FRLHziDxKjalEfn35wc1G/HT8YSK
+MMcONOGJJ8RjqSCpX7bYw++AIMszOELZawZKyJQ9uWYPsd3lGg1m0DJ5GxC/12GPtZOJsPIUogzu
+oNlz2YN0VlNjw4ERwxnQS56uU9siAxyw1xGA4tNH7n/EbCD2x1mg3ydZ/2JwyGYP2sMImF8iipc0
+eYgfT2Vv/7RbFjlunghjszdlHlhljWVH3m8/uAjkve75TxHIP0Ugf5kikHuUE5UApvThyKP6iw0E
+QBTz64dylQ15Y3cnsVGsVdYkiQYn/gvcgDhOqirWLCYt1MfmbUtizeSGvcgJg+QtEBh8RQ8chVlq
+4yn9A0vMXKlPxGxK0GR57EiIPpoq+9lOXlcBVdjNHrL6NYwyTkB5ibsygryDQcVhYWizI8JquVtx
+Hl9MwBl24PSRSAisPyWgmIoYf2yj1HGz4HZ3Vkkp2tMRKyV24ri7TTcbKPFM1pplvAfmvbdzxITT
+T2YU4BlYNkiyTPzV4ARFHUhkIIqiCacXrCoS0YaKaWr5vDYksiX/xhwVqP1/O2e32yYQROH7fQpk
+KelNaQA7TVW6lnrTF2h7XYEgjpWAkXGUKk/fmeFvzQKmaVRZyvkUyXgJy3J2dnYHxqy8yygrwiS9
+jR7l/FL7xfLrRfCN/qhVZclPzI1giYq3GcUuJW0Env+RPurD3er07ifvQ5FvpGLWfVPpQ3ObNL8e
+bRz09wcg9WnId7u49LPDD0BD8cNuQouXveQISTFVFTruUxrfbw9ubUL7KNnybXGR2812z4M7rDKt
+qEM6s2Kdr47658vVIRk0BdPSxRzZLw4YgumeFp3piOEcn6loD6eqhv1Q7YWa79/J/zg/8y3NP73S
+1lNp9f5HdLfLolFnJcIMeMLahcQ0JPmunhfSOsB0K8fetnF3fuwnfmoZeQ+qimaP/S7frI/XVzwD
+SPH4sfzYvldVp9w/6aaVqdwp3ZaWbsF1q5tWnXL+tSHnhHSxYEmnlbXknJBjXDaaHzhv7SEqSxbq
+OXHrldnCnpCCOEiD25HZUNTTqtXPktl5V0lHW42WzpiKq+K3Vn0dg06wZq6g2ao43U6t7Ja+sJ1a
+2S2d007Os5I0DXJwi7WEBloZwcFAAti8OKFKyVFVUk6VnOQY2Tm04NpvZJW2+BU/RPl9l3o1XD/N
+OL0zcJqOEY3Mj0X4FwqT0cj8WIR8exeNTGtl5CgNysG5LycFkWs+Gd/wwGkuvEoIe8mYEUvU6jXG
+jFgiafU3tjgVmGkllyXLyRm+xJwU6UuzWKLNKOYF1tr4L9kve6i8XbAi6+rss66momVkXb056vyv
+PH06h/e/Lb0b35P3v3krvP/tf1D3/yErzqX//er9f+h/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABg
+lD9gaWU0AHgAAA==
+--=-=-=
+Content-Type: text/plain
+
+
+PS: don't we have a "you forgot to actually attach the damn file" plugin
+when we detect the word "attachment" and there's no attach? :p
+
+--=-=-=--
index 75b05fa4e252c2d955b6738ab25a8dcef60b6427..ce174b52a4caae3ff92dfca7ff8a913076a7bb98 100644 (file)
@@ -63,7 +63,7 @@ and <a href=3D"http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg01680=
 
 --0016e687869333b14e0478963d33--
 --0016e687869333b1570478963d35
-Content-Type: application/octet-stream; 
+Content-Type: text/x-diff;
        name="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
 Content-Disposition: attachment; 
        filename="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
diff --git a/test/corpora/duplicate/msg-1-1:2, b/test/corpora/duplicate/msg-1-1:2,
new file mode 100644 (file)
index 0000000..bc7da06
--- /dev/null
@@ -0,0 +1,85 @@
+Return-path: <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>
+Envelope-to: david@tethera.net
+Delivery-date: Wed, 28 Nov 2012 18:41:46 -0400
+Received: from [199.188.72.155] (helo=yantan.tethera.net)
+       by tesseract.cs.unb.ca with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:32)
+       (Exim 4.72)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       id 1TdqKA-00017j-3c
+       for david@tethera.net; Wed, 28 Nov 2012 18:41:46 -0400
+Received: from wagner.debian.org ([217.196.43.132])
+       by yantan.tethera.net with esmtp (Exim 4.72)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       id 1TdqK9-00072z-AF
+       for david@tethera.net; Wed, 28 Nov 2012 18:41:45 -0400
+Received: from localhost ([::1] helo=wagner.debian.org)
+       by wagner.debian.org with esmtp (Exim 4.72)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       id 1TdqK8-0007GZ-67
+       for david@tethera.net; Wed, 28 Nov 2012 22:41:44 +0000
+Received: from vasks.debian.org ([217.196.43.140])
+       by wagner.debian.org with esmtp (Exim 4.72)
+       (envelope-from <gladky-anton-guest@alioth.debian.org>)
+       id 1TdqIc-0006jm-OC; Wed, 28 Nov 2012 22:40:11 +0000
+Received: from gladky-anton-guest by vasks.debian.org with local (Exim 4.72)
+       (envelope-from <gladky-anton-guest@vasks.debian.org>)
+       id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000
+Date: Wed, 28 Nov 2012 22:40:10 +0000
+From: Anton Gladky <gladky.anton@gmail.com>
+To: 691896@bugs.debian.org, control@bugs.debian.org,
+       691896-submitter@bugs.debian.org
+Message-ID: <debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15>
+X-PTS-Approved: Yes
+X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on wagner.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00,FREEMAIL_FROM
+       autolearn=ham version=3.3.1
+Subject: [87ea161] Fix for Bug#691896 committed to git
+X-BeenThere: debian-science-maintainers@lists.alioth.debian.org
+X-Mailman-Version: 2.1.13
+Precedence: list
+List-Id: Mailing list for maintainer discussions and BTS messages
+       <debian-science-maintainers.lists.alioth.debian.org>
+List-Unsubscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/options/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=unsubscribe>
+List-Archive: <http://lists.alioth.debian.org/pipermail/debian-science-maintainers>
+List-Post: <mailto:debian-science-maintainers@lists.alioth.debian.org>
+List-Help: <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=help>
+List-Subscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=subscribe>
+MIME-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org
+Errors-To: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org
+X-SA-Exim-Connect-IP: ::1
+X-SA-Exim-Mail-From: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org
+X-SA-Exim-Scanned: No (on wagner.debian.org); SAEximRunCond expanded to false
+X-Spam-Score: 1.3
+X-Spam_bar: +
+
+
+tags 691896 + pending
+thanks
+
+Hello,
+
+     The following change has been committed for this bug by
+ Anton Gladky <gladky.anton@gmail.com> on Wed, 31 Oct 2012 08:16:42 +0100.
+ The fix will be in the next upload. 
+====================================
+Minor fixes in README.Debian. (Closes: #691896)
+
+
+====================================
+
+You can check the diff of the fix at:
+
+    ;a=commitdiff;h=87ea161
+
+
+
+-- 
+debian-science-maintainers mailing list
+debian-science-maintainers@lists.alioth.debian.org
+http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers
diff --git a/test/corpora/duplicate/msg-1-2:2, b/test/corpora/duplicate/msg-1-2:2,
new file mode 100644 (file)
index 0000000..dc0476e
--- /dev/null
@@ -0,0 +1,113 @@
+Return-path: <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>
+Envelope-to: david@tethera.net
+Delivery-date: Wed, 28 Nov 2012 18:42:39 -0400
+Received: from [199.188.72.155] (helo=yantan.tethera.net)
+       by tesseract.cs.unb.ca with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:32)
+       (Exim 4.72)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       id 1TdqL1-00017s-3b
+       for david@tethera.net; Wed, 28 Nov 2012 18:42:39 -0400
+Received: from wagner.debian.org ([217.196.43.132])
+       by yantan.tethera.net with esmtp (Exim 4.72)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       id 1TdqL0-00073Z-Gw
+       for david@tethera.net; Wed, 28 Nov 2012 18:42:38 -0400
+Received: from localhost ([::1] helo=alioth.debian.org)
+       by wagner.debian.org with esmtp (Exim 4.72)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       id 1TdqKz-0007sR-PR
+       for david@tethera.net; Wed, 28 Nov 2012 22:42:37 +0000
+Received: from buxtehude.debian.org ([140.211.166.26])
+       by wagner.debian.org with esmtp (Exim 4.72)
+       (envelope-from <debbugs@buxtehude.debian.org>) id 1TdqKU-0007SW-IG
+       for debian-science-maintainers@lists.alioth.debian.org;
+       Wed, 28 Nov 2012 22:42:07 +0000
+Received: from debbugs by buxtehude.debian.org with local (Exim 4.72)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1TdqKR-0001dH-UL; Wed, 28 Nov 2012 22:42:03 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Anton Gladky <gladky.anton@gmail.com>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Science Maintainers
+       <debian-science-maintainers@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Wed, 28 Nov 2012 22:42:02 +0000
+Resent-Message-ID: <handler.691896.B691896.13541424145158@bugs.debian.org>
+X-Debian-PR-Message: followup 691896
+X-Debian-PR-Package: gmsh
+X-Debian-PR-Keywords: pending
+X-Debian-PR-Source: gmsh
+Received: via spool by 691896-submit@bugs.debian.org id=B691896.13541424145158
+       (code B ref 691896); Wed, 28 Nov 2012 22:42:02 +0000
+Received: (at 691896) by bugs.debian.org; 28 Nov 2012 22:40:14 +0000
+Received: from wagner.debian.org ([217.196.43.132])
+       by buxtehude.debian.org with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:32)
+       (Exim 4.72) (envelope-from <gladky-anton-guest@alioth.debian.org>)
+       id 1TdqIg-0001Kh-Ba; Wed, 28 Nov 2012 22:40:14 +0000
+Received: from vasks.debian.org ([217.196.43.140])
+       by wagner.debian.org with esmtp (Exim 4.72)
+       (envelope-from <gladky-anton-guest@alioth.debian.org>)
+       id 1TdqIc-0006jm-OC; Wed, 28 Nov 2012 22:40:11 +0000
+Received: from gladky-anton-guest by vasks.debian.org with local (Exim 4.72)
+       (envelope-from <gladky-anton-guest@vasks.debian.org>)
+       id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000
+Date: Wed, 28 Nov 2012 22:40:10 +0000
+From: Anton Gladky <gladky.anton@gmail.com>
+To: 691896@bugs.debian.org, control@bugs.debian.org,
+       691896-submitter@bugs.debian.org
+Message-ID: <debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15>
+X-PTS-Approved: Yes
+Resent-Sender: Debian BTS <debbugs@buxtehude.debian.org>
+X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on wagner.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-4.2 required=5.0 tests=BAYES_00,FREEMAIL_FROM,
+       RCVD_IN_DNSWL_MED autolearn=ham version=3.3.1
+Subject: Bug#691896: [87ea161] Fix for Bug#691896 committed to git
+x-debian-approved: yes
+X-BeenThere: debian-science-maintainers@lists.alioth.debian.org
+X-Mailman-Version: 2.1.13
+Precedence: list
+Reply-To: Anton Gladky <gladky.anton@gmail.com>, 691896@bugs.debian.org
+List-Id: Mailing list for maintainer discussions and BTS messages
+       <debian-science-maintainers.lists.alioth.debian.org>
+List-Unsubscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/options/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=unsubscribe>
+List-Archive: <http://lists.alioth.debian.org/pipermail/debian-science-maintainers>
+List-Post: <mailto:debian-science-maintainers@lists.alioth.debian.org>
+List-Help: <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=help>
+List-Subscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=subscribe>
+MIME-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org
+Errors-To: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org
+X-SA-Exim-Connect-IP: ::1
+X-SA-Exim-Mail-From: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org
+X-SA-Exim-Scanned: No (on wagner.debian.org); SAEximRunCond expanded to false
+X-Spam-Score: 1.3
+X-Spam_bar: +
+
+
+tags 691896 + pending
+thanks
+
+Hello,
+
+     The following change has been committed for this bug by
+ Anton Gladky <gladky.anton@gmail.com> on Wed, 31 Oct 2012 08:16:42 +0100.
+ The fix will be in the next upload. 
+====================================
+Minor fixes in README.Debian. (Closes: #691896)
+
+
+====================================
+
+You can check the diff of the fix at:
+
+    ;a=commitdiff;h=87ea161
+
+-- 
+debian-science-maintainers mailing list
+debian-science-maintainers@lists.alioth.debian.org
+http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers
diff --git a/test/corpora/duplicate/msg-2-1:2, b/test/corpora/duplicate/msg-2-1:2,
new file mode 100644 (file)
index 0000000..e118d78
--- /dev/null
@@ -0,0 +1,43 @@
+From: David Bremner <bremner@debian.org>
+To: Samuel Bronson <naesten@gmail.com>, 695159@bugs.debian.org, Debian Bug Tracking System <submit@bugs.debian.org>
+Subject: Re: Bug#695159: debian-el: Shouldn't put downloaded bugs loose in ~/
+In-Reply-To: <87vcch7hxy.fsf@naesten.dyndns.org>
+References: <87vcch7hxy.fsf@naesten.dyndns.org>
+Date: Thu, 25 Oct 2018 10:41:38 -0300
+Message-ID: <87r2geywh9.fsf@tethera.net>
+MIME-Version: 1.0
+Content-Type: text/plain
+
+
+Control: severity -1 minor
+Control: tag -1 moreinfo
+
+Samuel Bronson <naesten@gmail.com> writes:
+
+> Package: debian-el
+> Version: 35.2+nmu1
+> Severity: normal
+> File: /usr/share/emacs/site-lisp/debian-el/debian-bug.el
+>
+> Dear Maintainer,
+>
+> After being mildly annoyed with this for ages, it finally occurred to me
+> to file a bug about it:
+>
+> It's rather rude of `getdebian-bug-get-bug-as-email' to default to
+> sticking downloaded mbox files loose in ~/, isn't it?
+>
+> (And might it not make sense to try and use the same files as the bts(1)
+> command from the devscripts package?)
+>
+
+Hi Samuel
+
+There is already a variable "debian-bug-download-directory" which can be
+customized. Is there an obviously better default? I guess we could put
+things in /tmp by default, with a minor privacy leak on multiuser
+systems.
+
+d
+
+# body 1
diff --git a/test/corpora/duplicate/msg-2-2:2, b/test/corpora/duplicate/msg-2-2:2,
new file mode 100644 (file)
index 0000000..a7549c4
--- /dev/null
@@ -0,0 +1,140 @@
+Return-path: <bounces+20181025-bremner=debian.org@tracker.debian.org>
+Envelope-to: david@tethera.net
+Delivery-date: Thu, 25 Oct 2018 09:45:10 -0400
+Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33])
+       by fethera.tethera.net with esmtp (Exim 4.89)
+       (envelope-from <bounces+20181025-bremner=debian.org@tracker.debian.org>)
+       id 1gFfwj-0004Y9-69
+       for david@tethera.net; Thu, 25 Oct 2018 09:45:10 -0400
+Received: from ticharich.debian.org ([2001:41c8:1000:21::21:23])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=ticharich.debian.org,EMAIL=hostmaster@ticharich.debian.org (verified)
+       by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <bounces+20181025-bremner=debian.org@tracker.debian.org>)
+       id 1gFfwi-0004A1-J6
+       for david@tethera.net; Thu, 25 Oct 2018 13:45:08 +0000
+Received: from localhost ([::1] helo=ticharich.debian.org)
+       by ticharich.debian.org with esmtp (Exim 4.89)
+       (envelope-from <bounces+20181025-bremner=debian.org@tracker.debian.org>)
+       id 1gFfwh-0002Ex-6w
+       for david@tethera.net; Thu, 25 Oct 2018 13:45:07 +0000
+Received: from mailly.debian.org ([2001:41b8:202:deb:6564:a62:52c3:4b72])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=mailly.debian.org,EMAIL=hostmaster@mailly.debian.org (verified)
+       by ticharich.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1gFfwg-0002Em-NI
+       for dispatch+emacs-goodies-el@tracker.debian.org; Thu, 25 Oct 2018 13:45:06 +0000
+Received: from quantz.debian.org ([2001:41c8:1000:21::21:28])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=quantz.debian.org,EMAIL=hostmaster@quantz.debian.org (verified)
+       by mailly.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1gFfwg-0004h1-AC
+       for dispatch+emacs-goodies-el@tracker.debian.org; Thu, 25 Oct 2018 13:45:06 +0000
+Received: from qa by quantz.debian.org with local (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1gFfwf-0007Lg-TQ
+       for dispatch+emacs-goodies-el@tracker.debian.org; Thu, 25 Oct 2018 13:45:05 +0000
+Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=buxtehude.debian.org,EMAIL=hostmaster@buxtehude.debian.org (verified)
+       by quantz.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1gFfwf-0007J0-3r; Thu, 25 Oct 2018 13:45:05 +0000
+Received: from debbugs by buxtehude.debian.org with local (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1gFfwc-0003jj-VU; Thu, 25 Oct 2018 13:45:02 +0000
+X-Loop: owner@bugs.debian.org
+Subject: Bug#695159: debian-el: Shouldn't put downloaded bugs loose in ~/
+Reply-To: David Bremner <bremner@debian.org>, 695159@bugs.debian.org
+Resent-From: David Bremner <bremner@debian.org>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacsen team <debian-emacsen@lists.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 25 Oct 2018 13:45:01 +0000
+Resent-Message-ID: <handler.695159.B.154047490313415@bugs.debian.org>
+X-Debian-PR-Message: followup 695159
+X-Debian-PR-Package: debian-el
+X-Debian-PR-Keywords: 
+References: <87vcch7hxy.fsf@naesten.dyndns.org> <87vcch7hxy.fsf@naesten.dyndns.org>
+X-Debian-PR-Source: debian-el, emacs-goodies-el
+Received: via spool by submit@bugs.debian.org id=B.154047490313415
+          (code B); Thu, 25 Oct 2018 13:45:01 +0000
+Received: (at submit) by bugs.debian.org; 25 Oct 2018 13:41:43 +0000
+X-Spam-Checker-Version: SpamAssassin 3.4.1-bugs.debian.org_2005_01_02
+       (2015-04-28) on buxtehude.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-19.5 required=4.0 tests=BAYES_00,FROMDEVELOPER,GMAIL,
+       HAS_BUG_NUMBER,TXREP autolearn=ham autolearn_force=no
+       version=3.4.1-bugs.debian.org_2005_01_02
+X-Spam-Bayes: score:0.0000 Tokens: new, 16; hammy, 110; neutral, 41; spammy,
+       2. spammytokens:0.971-+--privacy, 0.857-+--customized
+       hammytokens:0.000-+--UD:el, 0.000-+--H*F:U*bremner, 0.000-+--Maintainer,
+       0.000-+--sitelisp, 0.000-+--site-lisp
+Received: from fethera.tethera.net ([2607:5300:60:c5::1])
+       by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <bremner@debian.org>)
+       id 1gFftO-0003U6-RV; Thu, 25 Oct 2018 13:41:42 +0000
+Received: from remotemail by fethera.tethera.net with local (Exim 4.89)
+       (envelope-from <bremner@debian.org>)
+       id 1gFftL-0004VK-V8; Thu, 25 Oct 2018 09:41:39 -0400
+Received: (nullmailer pid 477 invoked by uid 1000);
+       Thu, 25 Oct 2018 13:41:38 -0000
+From: David Bremner <bremner@debian.org>
+To: Samuel Bronson <naesten@gmail.com>, 695159@bugs.debian.org, Debian Bug Tracking System <submit@bugs.debian.org>
+In-Reply-To: <87vcch7hxy.fsf@naesten.dyndns.org>
+Date: Thu, 25 Oct 2018 10:41:38 -0300
+Message-ID: <87r2geywh9.fsf@tethera.net>
+MIME-Version: 1.0
+Content-Type: text/plain
+Delivered-To: submit@bugs.debian.org
+Delivered-To: emacs-goodies-el@packages.qa.debian.org
+Delivered-To: dispatch+emacs-goodies-el@tracker.debian.org
+X-Loop: dispatch@tracker.debian.org
+X-Distro-Tracker-Keyword: bts
+X-Distro-Tracker-Package: emacs-goodies-el
+List-Id: <emacs-goodies-el.tracker.debian.org>
+X-Debian: tracker.debian.org
+X-Debian-Package: emacs-goodies-el
+X-PTS-Package: emacs-goodies-el
+X-PTS-Keyword: bts
+X-Distro-Tracker-Team: emacsen
+X-Spam_score: -2.3
+X-Spam_score_int: -22
+X-Spam_bar: --
+
+
+Control: severity -1 minor
+Control: tag -1 moreinfo
+
+Samuel Bronson <naesten@gmail.com> writes:
+
+> Package: debian-el
+> Version: 35.2+nmu1
+> Severity: normal
+> File: /usr/share/emacs/site-lisp/debian-el/debian-bug.el
+>
+> Dear Maintainer,
+>
+> After being mildly annoyed with this for ages, it finally occurred to me
+> to file a bug about it:
+>
+> It's rather rude of `getdebian-bug-get-bug-as-email' to default to
+> sticking downloaded mbox files loose in ~/, isn't it?
+>
+> (And might it not make sense to try and use the same files as the bts(1)
+> command from the devscripts package?)
+>
+
+Hi Samuel
+
+There is already a variable "debian-bug-download-directory" which can be
+customized. Is there an obviously better default? I guess we could put
+things in /tmp by default, with a minor privacy leak on multiuser
+systems.
+
+d
+
+# body 2
diff --git a/test/corpora/duplicate/msg-3-1:2, b/test/corpora/duplicate/msg-3-1:2,
new file mode 100644 (file)
index 0000000..8bb6a8c
--- /dev/null
@@ -0,0 +1,183 @@
+Return-path: <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+Envelope-to: david@tethera.net
+Delivery-date: Thu, 20 Dec 2018 13:27:11 -0500
+Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33])
+       by fethera.tethera.net with esmtp (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32M-0005ae-Ko
+       for david@tethera.net; Thu, 20 Dec 2018 13:27:11 -0500
+Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1])
+       by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32K-0008PT-Nm
+       for david@tethera.net; Thu, 20 Dec 2018 18:27:08 +0000
+Received: from localhost ([::1] helo=alioth-lists-01.debian.net)
+       by alioth-lists-01.debian.net with esmtp (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32I-0003to-Kn
+       for bremner@debian.org; Thu, 20 Dec 2018 18:27:06 +0000
+Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39])
+ by alioth-lists-01.debian.net with esmtps
+ (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>) id 1ga32H-0003tO-29
+ for pkg-emacsen-addons@lists.alioth.debian.org; Thu, 20 Dec 2018 18:27:05 +0000
+Received: from debbugs by buxtehude.debian.org with local (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>)
+ id 1ga32F-0005PP-9m; Thu, 20 Dec 2018 18:27:03 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team
+ <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:02 +0000
+Resent-Message-ID: <handler.916805.B916805.154533033319811@bugs.debian.org>
+X-Debian-PR-Message: followup 916805
+X-Debian-PR-Package: src:assess-el
+X-Debian-PR-Keywords: ftbfs
+References: <87k1k6h43h.fsf@zephyr.silentflame.com>
+X-Debian-PR-Source: assess-el
+Received: via spool by 916805-submit@bugs.debian.org id=B916805.154533033319811
+ (code B ref 916805); Thu, 20 Dec 2018 18:27:02 +0000
+Received: (at 916805) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000
+X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02
+ (2018-09-13) on buxtehude.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-8.8 required=4.0 tests=BAYES_00,DKIM_SIGNED,
+ DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW,
+ SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP
+ autolearn=ham autolearn_force=no
+ version=3.4.2-bugs.debian.org_2005_01_02
+X-Spam-Bayes: score:0.0000 Tokens: new, 32; hammy, 83; neutral, 22; spammy, 1.
+ spammytokens:0.902-+--emails
+ hammytokens:0.000-+--HX-ME-Sender:xms, 
+ 0.000-+--H*RU:10.202.2.43,
+ 0.000-+--Hx-spam-relays-external:10.202.2.43, 0.000-+--H*F:U*spwhitton,
+ 0.000-+--H*F:D*spwhitton.name
+Received: from wout2-smtp.messagingengine.com ([64.147.123.25])
+ by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+ (Exim 4.89) (envelope-from <spwhitton@spwhitton.name>)
+ id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000
+Received: from compute3.internal (compute3.nyi.internal [10.202.2.43])
+ by mailout.west.internal (Postfix) with ESMTP id C50B712F6;
+ Thu, 20 Dec 2018 13:25:30 -0500 (EST)
+Received: from mailfrontend1 ([10.202.2.162])
+ by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name;
+ h=from:to:subject:date:message-id:mime-version:content-type; s=
+ fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14
+ zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/
+ EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/
+ D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw
+ BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6
+ BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o
+ fEnBaUBTAkTAHA==
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=
+ messagingengine.com; h=content-type:date:from:message-id
+ :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender
+ :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq
+ nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH
+ /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL
+ 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS
+ 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7
+ Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S
+ b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A==
+X-ME-Sender: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org,
+ 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org,
+ 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org,
+ 916876@bugs.debian.org
+Date: Thu, 20 Dec 2018 18:25:26 +0000
+Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com>
+MIME-Version: 1.0
+Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39;
+ envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org
+x-debian-approved: yes
+Subject: [Pkg-emacsen-addons] Bug#916805: Increase severity to 'serious'
+X-BeenThere: pkg-emacsen-addons@alioth-lists.debian.net
+X-Mailman-Version: 2.1.23
+Precedence: list
+List-Id: Maintainers list for Emacs addon packages
+ <pkg-emacsen-addons.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/pkg-emacsen-addons>, 
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/pkg-emacsen-addons/>
+List-Post: <mailto:pkg-emacsen-addons@alioth-lists.debian.net>
+List-Help: <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons>,
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 916805@bugs.debian.org
+Content-Type: multipart/mixed; boundary="===============5317466403067656157=="
+Errors-To: pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net
+Sender: "Pkg-emacsen-addons"
+ <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+X-Spam_score: 2.8
+X-Spam_score_int: 28
+X-Spam_bar: ++
+
+--===============5317466403067656157==
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha512; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+Content-Transfer-Encoding: quoted-printable
+
+control: severity -1 serious
+
+Hello,
+
+Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for
+all the e-mails).
+
+=2D-=20
+Sean Whitton
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G
+YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU
+gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2
+lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t
+FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi
+FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH
+TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w
+TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog
+ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM
+a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab
+VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE
+3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810=
+=mWfF
+-----END PGP SIGNATURE-----
+--=-=-=--
+
+
+--===============5317466403067656157==
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-Disposition: inline
+
+X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX18KUGtnLWVtYWNz
+ZW4tYWRkb25zIG1haWxpbmcgbGlzdApQa2ctZW1hY3Nlbi1hZGRvbnNAYWxpb3RoLWxpc3RzLmRl
+Ymlhbi5uZXQKaHR0cHM6Ly9hbGlvdGgtbGlzdHMuZGViaWFuLm5ldC9jZ2ktYmluL21haWxtYW4v
+bGlzdGluZm8vcGtnLWVtYWNzZW4tYWRkb25zCg==
+
+--===============5317466403067656157==--
+
diff --git a/test/corpora/duplicate/msg-3-2:2, b/test/corpora/duplicate/msg-3-2:2,
new file mode 100644 (file)
index 0000000..292d515
--- /dev/null
@@ -0,0 +1,184 @@
+Return-path: <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+Envelope-to: david@tethera.net
+Delivery-date: Thu, 20 Dec 2018 13:27:12 -0500
+Received: from mailly.debian.org ([2001:41b8:202:deb:6564:a62:52c3:4b72])
+       by fethera.tethera.net with esmtp (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32O-0005ap-Hn
+       for david@tethera.net; Thu, 20 Dec 2018 13:27:12 -0500
+Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1])
+       by mailly.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32O-0004V7-4x
+       for david@tethera.net; Thu, 20 Dec 2018 18:27:12 +0000
+Received: from localhost ([::1] helo=alioth-lists-01.debian.net)
+       by alioth-lists-01.debian.net with esmtp (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32N-0003wA-Sc
+       for bremner@debian.org; Thu, 20 Dec 2018 18:27:11 +0000
+Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39])
+ by alioth-lists-01.debian.net with esmtps
+ (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>) id 1ga32M-0003vQ-Q9
+ for pkg-emacsen-addons@lists.alioth.debian.org; Thu, 20 Dec 2018 18:27:10 +0000
+Received: from debbugs by buxtehude.debian.org with local (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>)
+ id 1ga32K-0005Sn-7B; Thu, 20 Dec 2018 18:27:08 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team
+ <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:06 +0000
+Resent-Message-ID: <handler.916808.B916808.154533033319830@bugs.debian.org>
+X-Debian-PR-Message: followup 916808
+X-Debian-PR-Package: src:hydra-el
+X-Debian-PR-Keywords: ftbfs
+References: <87bm5ih3w5.fsf@zephyr.silentflame.com>
+X-Debian-PR-Source: hydra-el
+Received: via spool by 916808-submit@bugs.debian.org id=B916808.154533033319830
+ (code B ref 916808); Thu, 20 Dec 2018 18:27:06 +0000
+Received: (at 916808) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000
+X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02
+ (2018-09-13) on buxtehude.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-11.3 required=4.0 tests=BAYES_00,DKIM_SIGNED,
+ DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW,
+ SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP
+ autolearn=unavailable autolearn_force=no
+ version=3.4.2-bugs.debian.org_2005_01_02
+X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1.
+ spammytokens:0.902-+--emails
+ hammytokens:0.000-+--HX-ME-Sender:xms, 
+ 0.000-+--H*RU:10.202.2.43,
+ 0.000-+--Hx-spam-relays-external:10.202.2.43, 0.000-+--H*F:U*spwhitton,
+ 0.000-+--H*F:D*spwhitton.name
+Received: from wout2-smtp.messagingengine.com ([64.147.123.25])
+ by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+ (Exim 4.89) (envelope-from <spwhitton@spwhitton.name>)
+ id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000
+Received: from compute3.internal (compute3.nyi.internal [10.202.2.43])
+ by mailout.west.internal (Postfix) with ESMTP id C50B712F6;
+ Thu, 20 Dec 2018 13:25:30 -0500 (EST)
+Received: from mailfrontend1 ([10.202.2.162])
+ by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name;
+ h=from:to:subject:date:message-id:mime-version:content-type; s=
+ fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14
+ zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/
+ EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/
+ D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw
+ BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6
+ BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o
+ fEnBaUBTAkTAHA==
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=
+ messagingengine.com; h=content-type:date:from:message-id
+ :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender
+ :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq
+ nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH
+ /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL
+ 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS
+ 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7
+ Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S
+ b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A==
+X-ME-Sender: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org,
+ 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org,
+ 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org,
+ 916876@bugs.debian.org
+Date: Thu, 20 Dec 2018 18:25:26 +0000
+Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com>
+MIME-Version: 1.0
+X-CrossAssassin-Score: 3
+Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39;
+ envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org
+x-debian-approved: yes
+Subject: [Pkg-emacsen-addons] Bug#916808: Increase severity to 'serious'
+X-BeenThere: pkg-emacsen-addons@alioth-lists.debian.net
+X-Mailman-Version: 2.1.23
+Precedence: list
+List-Id: Maintainers list for Emacs addon packages
+ <pkg-emacsen-addons.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/pkg-emacsen-addons>, 
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/pkg-emacsen-addons/>
+List-Post: <mailto:pkg-emacsen-addons@alioth-lists.debian.net>
+List-Help: <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons>,
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 916808@bugs.debian.org
+Content-Type: multipart/mixed; boundary="===============8231894308137086149=="
+Errors-To: pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net
+Sender: "Pkg-emacsen-addons"
+ <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+X-Spam_score: 2.8
+X-Spam_score_int: 28
+X-Spam_bar: ++
+
+--===============8231894308137086149==
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha512; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+Content-Transfer-Encoding: quoted-printable
+
+control: severity -1 serious
+
+Hello,
+
+Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for
+all the e-mails).
+
+=2D-=20
+Sean Whitton
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G
+YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU
+gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2
+lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t
+FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi
+FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH
+TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w
+TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog
+ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM
+a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab
+VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE
+3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810=
+=mWfF
+-----END PGP SIGNATURE-----
+--=-=-=--
+
+
+--===============8231894308137086149==
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-Disposition: inline
+
+X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX18KUGtnLWVtYWNz
+ZW4tYWRkb25zIG1haWxpbmcgbGlzdApQa2ctZW1hY3Nlbi1hZGRvbnNAYWxpb3RoLWxpc3RzLmRl
+Ymlhbi5uZXQKaHR0cHM6Ly9hbGlvdGgtbGlzdHMuZGViaWFuLm5ldC9jZ2ktYmluL21haWxtYW4v
+bGlzdGluZm8vcGtnLWVtYWNzZW4tYWRkb25zCg==
+
+--===============8231894308137086149==--
+
diff --git a/test/corpora/duplicate/msg-3-3:2, b/test/corpora/duplicate/msg-3-3:2,
new file mode 100644 (file)
index 0000000..bff2473
--- /dev/null
@@ -0,0 +1,178 @@
+Return-path: <bounces+20181220-bremner=debian.org@tracker.debian.org>
+Envelope-to: david@tethera.net
+Delivery-date: Thu, 20 Dec 2018 13:27:16 -0500
+Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33])
+       by fethera.tethera.net with esmtp (Exim 4.89)
+       (envelope-from <bounces+20181220-bremner=debian.org@tracker.debian.org>)
+       id 1ga32S-0005aw-5h
+       for david@tethera.net; Thu, 20 Dec 2018 13:27:16 -0500
+Received: from ticharich.debian.org ([2001:41c8:1000:21::21:23])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=ticharich.debian.org,EMAIL=hostmaster@ticharich.debian.org (verified)
+       by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <bounces+20181220-bremner=debian.org@tracker.debian.org>)
+       id 1ga32R-0008QQ-PL
+       for david@tethera.net; Thu, 20 Dec 2018 18:27:15 +0000
+Received: from localhost ([::1] helo=ticharich.debian.org)
+       by ticharich.debian.org with esmtp (Exim 4.89)
+       (envelope-from <bounces+20181220-bremner=debian.org@tracker.debian.org>)
+       id 1ga32Q-0008BB-Ad
+       for david@tethera.net; Thu, 20 Dec 2018 18:27:14 +0000
+Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=muffat.debian.org,EMAIL=hostmaster@muffat.debian.org (verified)
+       by ticharich.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1ga32Q-0008Ai-2N
+       for dispatch+haskell-mode@tracker.debian.org; Thu, 20 Dec 2018 18:27:14 +0000
+Received: from quantz.debian.org ([2001:41c8:1000:21::21:28])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=quantz.debian.org,EMAIL=hostmaster@quantz.debian.org (verified)
+       by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1ga32O-0008QE-Eo
+       for dispatch+haskell-mode@tracker.debian.org; Thu, 20 Dec 2018 18:27:12 +0000
+Received: from qa by quantz.debian.org with local (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1ga32M-0006hb-Vn
+       for dispatch+haskell-mode@tracker.debian.org; Thu, 20 Dec 2018 18:27:10 +0000
+Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39])
+       from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=buxtehude.debian.org,EMAIL=hostmaster@buxtehude.debian.org (verified)
+       by quantz.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1ga32J-0006gG-1x
+       for haskell-mode@packages.qa.debian.org; Thu, 20 Dec 2018 18:27:07 +0000
+Received: from debbugs by buxtehude.debian.org with local (Exim 4.89)
+       (envelope-from <debbugs@buxtehude.debian.org>)
+       id 1ga32H-0005SF-MF; Thu, 20 Dec 2018 18:27:05 +0000
+X-Loop: owner@bugs.debian.org
+Subject: Bug#916807: Increase severity to 'serious'
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 916807@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:04 +0000
+Resent-Message-ID: <handler.916807.B916807.154533033319820@bugs.debian.org>
+X-Debian-PR-Message: followup 916807
+X-Debian-PR-Package: src:haskell-mode
+X-Debian-PR-Keywords: ftbfs
+References: <87efaeh3zr.fsf@zephyr.silentflame.com>
+X-Debian-PR-Source: haskell-mode
+Received: via spool by 916807-submit@bugs.debian.org id=B916807.154533033319820
+          (code B ref 916807); Thu, 20 Dec 2018 18:27:04 +0000
+Received: (at 916807) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000
+X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02
+       (2018-09-13) on buxtehude.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-11.3 required=4.0 tests=BAYES_00,DKIM_SIGNED,
+       DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW,
+       SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP
+       autolearn=unavailable autolearn_force=no
+       version=3.4.2-bugs.debian.org_2005_01_02
+X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1.
+       spammytokens:0.902-+--emails hammytokens:0.000-+--HX-ME-Sender:xms,
+       0.000-+--H*RU:10.202.2.43,
+       0.000-+--Hx-spam-relays-external:10.202.2.43,
+       0.000-+--H*F:D*spwhitton.name, 0.000-+--H*F:U*spwhitton
+Received: from wout2-smtp.messagingengine.com ([64.147.123.25])
+       by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <spwhitton@spwhitton.name>)
+       id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000
+Received: from compute3.internal (compute3.nyi.internal [10.202.2.43])
+       by mailout.west.internal (Postfix) with ESMTP id C50B712F6;
+       Thu, 20 Dec 2018 13:25:30 -0500 (EST)
+Received: from mailfrontend1 ([10.202.2.162])
+  by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name;
+        h=from:to:subject:date:message-id:mime-version:content-type; s=
+       fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14
+       zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/
+       EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/
+       D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw
+       BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6
+       BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o
+       fEnBaUBTAkTAHA==
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=
+       messagingengine.com; h=content-type:date:from:message-id
+       :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender
+       :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq
+       nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH
+       /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL
+       8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS
+       5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7
+       Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S
+       b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A==
+X-ME-Sender: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+    ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+    fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+    gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+    thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+    hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+    ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+    <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+    <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+    <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, 916876@bugs.debian.org
+Date: Thu, 20 Dec 2018 18:25:26 +0000
+Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha512; protocol="application/pgp-signature"
+X-CrossAssassin-Score: 2
+Delivered-To: haskell-mode@packages.qa.debian.org
+Delivered-To: dispatch+haskell-mode@tracker.debian.org
+X-Loop: dispatch@tracker.debian.org
+X-Distro-Tracker-Keyword: bts
+X-Distro-Tracker-Package: haskell-mode
+List-Id: <haskell-mode.tracker.debian.org>
+X-Debian: tracker.debian.org
+X-Debian-Package: haskell-mode
+X-PTS-Package: haskell-mode
+X-PTS-Keyword: bts
+Precedence: list
+List-Unsubscribe: <mailto:control@tracker.debian.org?body=unsubscribe%20haskell-mode>
+X-Spam_score: 2.8
+X-Spam_score_int: 28
+X-Spam_bar: ++
+
+--=-=-=
+Content-Type: text/plain
+Content-Transfer-Encoding: quoted-printable
+
+control: severity -1 serious
+
+Hello,
+
+Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for
+all the e-mails).
+
+=2D-=20
+Sean Whitton
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G
+YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU
+gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2
+lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t
+FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi
+FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH
+TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w
+TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog
+ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM
+a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab
+VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE
+3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810=
+=mWfF
+-----END PGP SIGNATURE-----
+--=-=-=--
+
diff --git a/test/corpora/duplicate/msg-3-4:2, b/test/corpora/duplicate/msg-3-4:2,
new file mode 100644 (file)
index 0000000..861719d
--- /dev/null
@@ -0,0 +1,184 @@
+Return-path: <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+Envelope-to: david@tethera.net
+Delivery-date: Thu, 20 Dec 2018 13:27:16 -0500
+Received: from mailly.debian.org ([2001:41b8:202:deb:6564:a62:52c3:4b72])
+       by fethera.tethera.net with esmtp (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32S-0005ax-Kz
+       for david@tethera.net; Thu, 20 Dec 2018 13:27:16 -0500
+Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1])
+       by mailly.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+       (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32S-0004W6-8i
+       for david@tethera.net; Thu, 20 Dec 2018 18:27:16 +0000
+Received: from localhost ([::1] helo=alioth-lists-01.debian.net)
+       by alioth-lists-01.debian.net with esmtp (Exim 4.89)
+       (envelope-from <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       id 1ga32S-0003xQ-0U
+       for bremner@debian.org; Thu, 20 Dec 2018 18:27:16 +0000
+Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39])
+ by alioth-lists-01.debian.net with esmtps
+ (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>) id 1ga32Q-0003wj-1x
+ for pkg-emacsen-addons@lists.alioth.debian.org; Thu, 20 Dec 2018 18:27:14 +0000
+Received: from debbugs by buxtehude.debian.org with local (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>)
+ id 1ga32O-0005Te-Sa; Thu, 20 Dec 2018 18:27:12 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team
+ <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:11 +0000
+Resent-Message-ID: <handler.916811.B916811.154533033319851@bugs.debian.org>
+X-Debian-PR-Message: followup 916811
+X-Debian-PR-Package: src:weechat-el
+X-Debian-PR-Keywords: ftbfs
+References: <8736quh3la.fsf@zephyr.silentflame.com>
+X-Debian-PR-Source: weechat-el
+Received: via spool by 916811-submit@bugs.debian.org id=B916811.154533033319851
+ (code B ref 916811); Thu, 20 Dec 2018 18:27:11 +0000
+Received: (at 916811) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000
+X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02
+ (2018-09-13) on buxtehude.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-15.2 required=4.0 tests=BAYES_00,DKIM_SIGNED,
+ DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW,
+ SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP
+ autolearn=unavailable autolearn_force=no
+ version=3.4.2-bugs.debian.org_2005_01_02
+X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1.
+ spammytokens:0.902-+--emails
+ hammytokens:0.000-+--HX-ME-Sender:xms, 
+ 0.000-+--H*RU:10.202.2.43,
+ 0.000-+--Hx-spam-relays-external:10.202.2.43,
+ 0.000-+--H*F:D*spwhitton.name, 0.000-+--H*F:U*spwhitton
+Received: from wout2-smtp.messagingengine.com ([64.147.123.25])
+ by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+ (Exim 4.89) (envelope-from <spwhitton@spwhitton.name>)
+ id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000
+Received: from compute3.internal (compute3.nyi.internal [10.202.2.43])
+ by mailout.west.internal (Postfix) with ESMTP id C50B712F6;
+ Thu, 20 Dec 2018 13:25:30 -0500 (EST)
+Received: from mailfrontend1 ([10.202.2.162])
+ by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name;
+ h=from:to:subject:date:message-id:mime-version:content-type; s=
+ fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14
+ zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/
+ EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/
+ D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw
+ BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6
+ BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o
+ fEnBaUBTAkTAHA==
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=
+ messagingengine.com; h=content-type:date:from:message-id
+ :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender
+ :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq
+ nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH
+ /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL
+ 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS
+ 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7
+ Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S
+ b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A==
+X-ME-Sender: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org,
+ 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org,
+ 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org,
+ 916876@bugs.debian.org
+Date: Thu, 20 Dec 2018 18:25:26 +0000
+Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com>
+MIME-Version: 1.0
+X-CrossAssassin-Score: 5
+Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39;
+ envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org
+x-debian-approved: yes
+Subject: [Pkg-emacsen-addons] Bug#916811: Increase severity to 'serious'
+X-BeenThere: pkg-emacsen-addons@alioth-lists.debian.net
+X-Mailman-Version: 2.1.23
+Precedence: list
+List-Id: Maintainers list for Emacs addon packages
+ <pkg-emacsen-addons.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/pkg-emacsen-addons>, 
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/pkg-emacsen-addons/>
+List-Post: <mailto:pkg-emacsen-addons@alioth-lists.debian.net>
+List-Help: <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons>,
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 916811@bugs.debian.org
+Content-Type: multipart/mixed; boundary="===============5359377007738902760=="
+Errors-To: pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net
+Sender: "Pkg-emacsen-addons"
+ <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+X-Spam_score: 2.8
+X-Spam_score_int: 28
+X-Spam_bar: ++
+
+--===============5359377007738902760==
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha512; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+Content-Transfer-Encoding: quoted-printable
+
+control: severity -1 serious
+
+Hello,
+
+Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for
+all the e-mails).
+
+=2D-=20
+Sean Whitton
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G
+YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU
+gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2
+lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t
+FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi
+FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH
+TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w
+TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog
+ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM
+a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab
+VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE
+3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810=
+=mWfF
+-----END PGP SIGNATURE-----
+--=-=-=--
+
+
+--===============5359377007738902760==
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-Disposition: inline
+
+X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX18KUGtnLWVtYWNz
+ZW4tYWRkb25zIG1haWxpbmcgbGlzdApQa2ctZW1hY3Nlbi1hZGRvbnNAYWxpb3RoLWxpc3RzLmRl
+Ymlhbi5uZXQKaHR0cHM6Ly9hbGlvdGgtbGlzdHMuZGViaWFuLm5ldC9jZ2ktYmluL21haWxtYW4v
+bGlzdGluZm8vcGtnLWVtYWNzZW4tYWRkb25zCg==
+
+--===============5359377007738902760==--
+
diff --git a/test/corpora/duplicate/msg-3-5:2, b/test/corpora/duplicate/msg-3-5:2,
new file mode 100644 (file)
index 0000000..af26374
--- /dev/null
@@ -0,0 +1,179 @@
+Return-path: <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>
+Envelope-to: david@tethera.net
+Delivery-date: Thu, 20 Dec 2018 13:27:21 -0500
+Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1])
+       by fethera.tethera.net with esmtp (Exim 4.89)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>)
+       id 1ga32X-0005bA-Go
+       for david@tethera.net; Thu, 20 Dec 2018 13:27:21 -0500
+Received: from localhost ([::1] helo=alioth-lists-01.debian.net)
+       by alioth-lists-01.debian.net with esmtp (Exim 4.89)
+       (envelope-from <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>)
+       id 1ga32U-0003yc-Ai
+       for david@tethera.net; Thu, 20 Dec 2018 18:27:18 +0000
+Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39])
+ by alioth-lists-01.debian.net with esmtps
+ (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>) id 1ga32S-0003xN-6J
+ for debian-science-maintainers@lists.alioth.debian.org;
+ Thu, 20 Dec 2018 18:27:16 +0000
+Received: from debbugs by buxtehude.debian.org with local (Exim 4.89)
+ (envelope-from <debbugs@buxtehude.debian.org>)
+ id 1ga32R-0005U9-0Z; Thu, 20 Dec 2018 18:27:15 +0000
+X-Loop: owner@bugs.debian.org
+Subject: Bug#916867: Increase severity to 'serious'
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Science Maintainers
+ <debian-science-maintainers@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:13 +0000
+Resent-Message-ID: <handler.916867.B916867.154533033319861@bugs.debian.org>
+X-Debian-PR-Message: followup 916867
+X-Debian-PR-Package: src:hkl
+X-Debian-PR-Keywords: ftbfs
+References: <87sgyt2xyg.fsf@zephyr.silentflame.com>
+X-Debian-PR-Source: hkl
+Received: via spool by 916867-submit@bugs.debian.org id=B916867.154533033319861
+ (code B ref 916867); Thu, 20 Dec 2018 18:27:13 +0000
+Received: (at 916867) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000
+X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02
+ (2018-09-13) on buxtehude.debian.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-13.0 required=4.0 tests=BAYES_00,DKIM_SIGNED,
+ DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW,
+ SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP
+ autolearn=unavailable autolearn_force=no
+ version=3.4.2-bugs.debian.org_2005_01_02
+X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1.
+ spammytokens:0.902-+--emails
+ hammytokens:0.000-+--HX-ME-Sender:xms, 
+ 0.000-+--H*RU:10.202.2.43,
+ 0.000-+--Hx-spam-relays-external:10.202.2.43,
+ 0.000-+--H*F:D*spwhitton.name, 0.000-+--H*F:U*spwhitton
+Received: from wout2-smtp.messagingengine.com ([64.147.123.25])
+ by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
+ (Exim 4.89) (envelope-from <spwhitton@spwhitton.name>)
+ id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000
+Received: from compute3.internal (compute3.nyi.internal [10.202.2.43])
+ by mailout.west.internal (Postfix) with ESMTP id C50B712F6;
+ Thu, 20 Dec 2018 13:25:30 -0500 (EST)
+Received: from mailfrontend1 ([10.202.2.162])
+ by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name;
+ h=from:to:subject:date:message-id:mime-version:content-type; s=
+ fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14
+ zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/
+ EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/
+ D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw
+ BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6
+ BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o
+ fEnBaUBTAkTAHA==
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=
+ messagingengine.com; h=content-type:date:from:message-id
+ :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender
+ :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq
+ nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH
+ /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL
+ 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS
+ 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7
+ Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S
+ b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A==
+X-ME-Sender: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org,
+ 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org,
+ 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org,
+ 916876@bugs.debian.org
+Date: Thu, 20 Dec 2018 18:25:26 +0000
+Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com>
+MIME-Version: 1.0
+X-CrossAssassin-Score: 6
+Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39;
+ envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org
+x-debian-approved: yes
+X-BeenThere: debian-science-maintainers@alioth-lists.debian.net
+X-Mailman-Version: 2.1.23
+Precedence: list
+List-Id: Mailing list for maintainer discussions and BTS messages
+ <debian-science-maintainers.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/debian-science-maintainers>,
+ <mailto:debian-science-maintainers-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/debian-science-maintainers/>
+List-Post: <mailto:debian-science-maintainers@alioth-lists.debian.net>
+List-Help: <mailto:debian-science-maintainers-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/debian-science-maintainers>,
+ <mailto:debian-science-maintainers-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 916867@bugs.debian.org
+Content-Type: multipart/mixed; boundary="===============7686561040995282884=="
+Errors-To: debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net
+Sender: "debian-science-maintainers"
+ <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>
+X-Spam_score: 2.8
+X-Spam_score_int: 28
+X-Spam_bar: ++
+
+--===============7686561040995282884==
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha512; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+Content-Transfer-Encoding: quoted-printable
+
+control: severity -1 serious
+
+Hello,
+
+Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for
+all the e-mails).
+
+=2D-=20
+Sean Whitton
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G
+YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU
+gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2
+lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t
+FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi
+FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH
+TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w
+TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog
+ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM
+a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab
+VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE
+3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810=
+=mWfF
+-----END PGP SIGNATURE-----
+--=-=-=--
+
+
+--===============7686561040995282884==
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-Disposition: inline
+
+LS0gCmRlYmlhbi1zY2llbmNlLW1haW50YWluZXJzIG1haWxpbmcgbGlzdApkZWJpYW4tc2NpZW5j
+ZS1tYWludGFpbmVyc0BhbGlvdGgtbGlzdHMuZGViaWFuLm5ldApodHRwczovL2FsaW90aC1saXN0
+cy5kZWJpYW4ubmV0L2NnaS1iaW4vbWFpbG1hbi9saXN0aW5mby9kZWJpYW4tc2NpZW5jZS1tYWlu
+dGFpbmVycw==
+
+--===============7686561040995282884==--
+
diff --git a/test/emacs-reply.expected-output/notmuch-reply-duplicate-4 b/test/emacs-reply.expected-output/notmuch-reply-duplicate-4
new file mode 100644 (file)
index 0000000..836f77b
--- /dev/null
@@ -0,0 +1,21 @@
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Sean Whitton <spwhitton@spwhitton.name>, 916811@bugs.debian.org, 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, 916876@bugs.debian.org
+Subject: Re: [Pkg-emacsen-addons] Bug#916811: Increase severity to 'serious'
+In-Reply-To: <87r2ecrr6x.fsf@zephyr.silentflame.com>
+Fcc: MAIL_DIR/sent
+--text follows this line--
+Sean Whitton <spwhitton@spwhitton.name> writes:
+
+> control: severity -1 serious
+>
+> Hello,
+>
+> Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for
+> all the e-mails).
+>
+> -- 
+> Sean Whitton
+> _______________________________________________
+> Pkg-emacsen-addons mailing list
+> Pkg-emacsen-addons@alioth-lists.debian.net
+> https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons
diff --git a/test/emacs-show.expected-output/notmuch-show-depth b/test/emacs-show.expected-output/notmuch-show-depth
new file mode 100644 (file)
index 0000000..8299519
--- /dev/null
@@ -0,0 +1,44 @@
+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 (hidden) ]
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+ Subject: Re: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 01:02:38 +0600
+
+ [ multipart/mixed (hidden) ]
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed (hidden) ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Wed, 18 Nov 2009 02:50:48 +0600
+
+   [ text/plain (hidden) ]
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Tue, 17 Nov 2009 13:24:13 -0800
+
+   [ text/plain (hidden) ]
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed (hidden) ]
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
+ Subject: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 02:08:10 -0800
+
+ [ text/plain (hidden) ]
diff --git a/test/emacs-show.expected-output/notmuch-show-depth-1 b/test/emacs-show.expected-output/notmuch-show-depth-1
new file mode 100644 (file)
index 0000000..e7c376b
--- /dev/null
@@ -0,0 +1,119 @@
+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 ]
+[ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+[ 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. ]
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+ Subject: Re: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 01:02:38 +0600
+
+ [ multipart/mixed ]
+ [ multipart/signed ]
+ [ Unknown key ID 0x9D20F6503E338888 or unsupported algorithm ]
+ [ text/plain ]
+
+ Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did
+ gyre and gimble:
+
+  LK> Resulted in 4604 lines of errors along the lines of:
+
+  LK>   Error opening
+  LK>  
+ /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+  LK>   Too many open files
+
+ See the patch just posted here.
+
+ [ 2-line signature. Click/Enter to show. ]
+ [ application/pgp-signature ]
+ [ text/plain ]
+ [ 4-line signature. Click/Enter to show. ]
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed (hidden) ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Wed, 18 Nov 2009 02:50:48 +0600
+
+   [ text/plain (hidden) ]
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Tue, 17 Nov 2009 13:24:13 -0800
+
+   [ text/plain (hidden) ]
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed (hidden) ]
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
+ Subject: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 02:08:10 -0800
+
+ On Tue, 17 Nov 2009 14:00:54 -0500, Lars Kellogg-Stedman <lars at
+ seas.harvard.edu> wrote:
+ > 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).
+
+ Welcome, Lars!
+
+ I hadn't even seen that Keith's blog post had been picked up by lwn.net.
+ That's very interesting. So, thanks for coming and trying out notmuch.
+
+ >   Error opening
+ > /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+ >   Too many open files
+
+ Sadly, the lwn article coincided with me having just introduced this
+ bug, and then getting on a Trans-Atlantic flight. So I fixed the bug
+ fairly quickly, but there was quite a bit of latency before I could push
+ the fix out. It should be fixed now.
+
+ > I'm curious if this is expected behavior (i.e., notmuch does not work
+ > with Maildir) or if something else is going on.
+
+ Notmuch works just fine with maildir---it's one of the things that it
+ likes the best.
+
+ Happy hacking,
+
+ -Carl
diff --git a/test/emacs-show.expected-output/notmuch-show-duplicate-4 b/test/emacs-show.expected-output/notmuch-show-duplicate-4
new file mode 100644 (file)
index 0000000..6bf49d8
--- /dev/null
@@ -0,0 +1,20 @@
+Sean Whitton <spwhitton@spwhitton.name> (2018-12-20) (inbox signed)         4/5
+Subject: [Pkg-emacsen-addons] Bug#916811: Increase severity to 'serious'
+To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, 916876@bugs.debian.org
+Date: Thu, 20 Dec 2018 18:25:26 +0000
+
+[ multipart/mixed ]
+[ multipart/signed ]
+[ Unknown key ID 0x695B7AE4BF066240 or unsupported algorithm ]
+[ text/plain ]
+control: severity -1 serious
+
+Hello,
+
+Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for
+all the e-mails).
+
+[ 2-line signature. Click/Enter to show. ]
+[ signature.asc: application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
diff --git a/test/emacs-show.expected-output/notmuch-show-height-0 b/test/emacs-show.expected-output/notmuch-show-height-0
new file mode 100644 (file)
index 0000000..d646353
--- /dev/null
@@ -0,0 +1,97 @@
+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 (hidden) ]
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+ Subject: Re: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 01:02:38 +0600
+
+ [ multipart/mixed (hidden) ]
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed (hidden) ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Wed, 18 Nov 2009 02:50:48 +0600
+
+   Twas brillig at 15:33:01 17.11.2009 UTC-05 when lars at seas.harvard.edu
+   did gyre and gimble:
+
+    LK> Is the list archived anywhere?  The obvious archives
+    LK> (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+    LK> think I subscribed too late to get the patch (I only just saw the
+    LK> discussion about it).
+
+    LK> It doesn't look like the patch is in git yet.
+
+   Just has been pushed
+
+   [ 10-line signature. Click/Enter to show. ]
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Tue, 17 Nov 2009 13:24:13 -0800
+
+   [ text/plain (hidden) ]
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed ]
+    [ multipart/signed ]
+    [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+    [ text/plain ]
+    > I've also pushed a slightly more complicated (and complete) fix to my
+    > private notmuch repository
+
+    The version of lib/messages.cc in your repo doesn't build because it's
+    missing "#include <stdint.h>" (for the uint32_t on line 466).
+
+    [ 4-line signature. Click/Enter to show. ]
+    [ application/pgp-signature ]
+    [ text/plain ]
+    [ 4-line signature. Click/Enter to show. ]
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
+ Subject: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 02:08:10 -0800
+
+ On Tue, 17 Nov 2009 14:00:54 -0500, Lars Kellogg-Stedman <lars at
+ seas.harvard.edu> wrote:
+ > 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).
+
+ Welcome, Lars!
+
+ I hadn't even seen that Keith's blog post had been picked up by lwn.net.
+ That's very interesting. So, thanks for coming and trying out notmuch.
+
+ >   Error opening
+ > /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+ >   Too many open files
+
+ Sadly, the lwn article coincided with me having just introduced this
+ bug, and then getting on a Trans-Atlantic flight. So I fixed the bug
+ fairly quickly, but there was quite a bit of latency before I could push
+ the fix out. It should be fixed now.
+
+ > I'm curious if this is expected behavior (i.e., notmuch does not work
+ > with Maildir) or if something else is going on.
+
+ Notmuch works just fine with maildir---it's one of the things that it
+ likes the best.
+
+ Happy hacking,
+
+ -Carl
diff --git a/test/emacs-show.expected-output/notmuch-show-multipart-alternative b/test/emacs-show.expected-output/notmuch-show-multipart-alternative
new file mode 100644 (file)
index 0000000..e2951d2
--- /dev/null
@@ -0,0 +1,62 @@
+Alex Botero-Lowry <alex.boterolowry@gmail.com> (2009-11-17) (attachment inbox)
+Subject: [notmuch] preliminary FreeBSD support
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 11:36:14 -0800
+
+[ multipart/mixed ]
+[ multipart/alternative ]
+[ text/plain ]
+I saw the announcement this morning, and was very excited, as I had been
+hoping sup would be turned into a library,
+since I like the concept more than the UI (I'd rather an emacs interface).
+
+I did a preliminary compile which worked out fine, but
+sysconf(_SC_SC_GETPW_R_SIZE_MAX) returns -1 on
+FreeBSD, so notmuch_config_open segfaulted.
+
+Attached is a patch that supplies a default buffer size of 64 in cases where
+-1 is returned.
+
+http://www.opengroup.org/austin/docs/austin_328.txt - seems to indicate this
+is acceptable behavior,
+and
+http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.htmlspecifically
+uses 64 as the
+buffer size.
+[ text/html (hidden) ]
+[ 0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch: text/x-diff ]
+From e3bc4bbd7b9d0d086816ab5f8f2d6ffea1dd3ea4 Mon Sep 17 00:00:00 2001
+From: Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Date: Tue, 17 Nov 2009 11:30:39 -0800
+Subject: [PATCH] Deal with situation where sysconf(_SC_GETPW_R_SIZE_MAX) returns -1
+
+---
+ notmuch-config.c |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/notmuch-config.c b/notmuch-config.c
+index 248149c..e7220d8 100644
+--- a/notmuch-config.c
++++ b/notmuch-config.c
+@@ -77,6 +77,7 @@ static char *
+ get_name_from_passwd_file (void *ctx)
+ {
+     long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
++    if (pw_buf_size == -1) pw_buf_size = 64;
+     char *pw_buf = talloc_zero_size (ctx, pw_buf_size);
+     struct passwd passwd, *ignored;
+     char *name;
+@@ -101,6 +102,7 @@ static char *
+ get_username_from_passwd_file (void *ctx)
+ {
+     long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
++    if (pw_buf_size == -1) pw_buf_size = 64;
+     char *pw_buf = talloc_zero_size (ctx, pw_buf_size);
+     struct passwd passwd, *ignored;
+     char *name;
+-- 
+1.6.5.2
+
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+ Carl Worth <cworth@cworth.org> (2009-11-17) (inbox unread)
diff --git a/test/emacs-show.expected-output/notmuch-show-size b/test/emacs-show.expected-output/notmuch-show-size
new file mode 100644 (file)
index 0000000..cdde467
--- /dev/null
@@ -0,0 +1,64 @@
+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 ]
+[ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+[ text/plain (hidden) ]
+[ application/pgp-signature ]
+[ text/plain (hidden) ]
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+ Subject: Re: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 01:02:38 +0600
+
+ [ multipart/mixed ]
+ [ multipart/signed ]
+ [ Unknown key ID 0x9D20F6503E338888 or unsupported algorithm ]
+ [ text/plain (hidden) ]
+ [ application/pgp-signature ]
+ [ text/plain (hidden) ]
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed ]
+  [ multipart/signed ]
+  [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+  [ text/plain (hidden) ]
+  [ application/pgp-signature ]
+  [ text/plain (hidden) ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Wed, 18 Nov 2009 02:50:48 +0600
+
+   [ text/plain (hidden) ]
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Tue, 17 Nov 2009 13:24:13 -0800
+
+   [ text/plain (hidden) ]
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed ]
+    [ multipart/signed ]
+    [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+    [ text/plain (hidden) ]
+    [ application/pgp-signature ]
+    [ text/plain (hidden) ]
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
+ Subject: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 02:08:10 -0800
+
+ [ text/plain (hidden) ]
diff --git a/test/emacs-show.expected-output/notmuch-show-size-450 b/test/emacs-show.expected-output/notmuch-show-size-450
new file mode 100644 (file)
index 0000000..ec34612
--- /dev/null
@@ -0,0 +1,89 @@
+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 ]
+[ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+[ text/plain (hidden) ]
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+ Subject: Re: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 01:02:38 +0600
+
+ [ multipart/mixed ]
+ [ multipart/signed ]
+ [ Unknown key ID 0x9D20F6503E338888 or unsupported algorithm ]
+ [ text/plain ]
+
+ Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did
+ gyre and gimble:
+
+  LK> Resulted in 4604 lines of errors along the lines of:
+
+  LK>   Error opening
+  LK>  
+ /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S:
+  LK>   Too many open files
+
+ See the patch just posted here.
+
+ [ 2-line signature. Click/Enter to show. ]
+ [ application/pgp-signature ]
+ [ text/plain ]
+ [ 4-line signature. Click/Enter to show. ]
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed ]
+  [ multipart/signed ]
+  [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+  [ text/plain (hidden) ]
+  [ application/pgp-signature ]
+  [ text/plain ]
+  [ 4-line signature. Click/Enter to show. ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Wed, 18 Nov 2009 02:50:48 +0600
+
+   [ text/plain (hidden) ]
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+   Subject: [notmuch] Working with Maildir storage?
+   To: notmuch@notmuchmail.org
+   Date: Tue, 17 Nov 2009 13:24:13 -0800
+
+   [ text/plain (hidden) ]
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed ]
+    [ multipart/signed ]
+    [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ]
+    [ text/plain ]
+    > I've also pushed a slightly more complicated (and complete) fix to my
+    > private notmuch repository
+
+    The version of lib/messages.cc in your repo doesn't build because it's
+    missing "#include <stdint.h>" (for the uint32_t on line 466).
+
+    [ 4-line signature. Click/Enter to show. ]
+    [ application/pgp-signature ]
+    [ text/plain ]
+    [ 4-line signature. Click/Enter to show. ]
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
+ Subject: [notmuch] Working with Maildir storage?
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 02:08:10 -0800
+
+ [ text/plain (hidden) ]
index 75b05fa4e252c2d955b6738ab25a8dcef60b6427..ce174b52a4caae3ff92dfca7ff8a913076a7bb98 100644 (file)
@@ -63,7 +63,7 @@ and <a href=3D"http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg01680=
 
 --0016e687869333b14e0478963d33--
 --0016e687869333b1570478963d35
-Content-Type: application/octet-stream; 
+Content-Type: text/x-diff;
        name="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
 Content-Disposition: attachment; 
        filename="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
index 59b6079dd14acf6fedddc4dcd668fd147fdfb396..d664812f610f91c3d38b1b96aa905ae756896c73 100644 (file)
@@ -522,11 +522,27 @@ notmuch_json_show_sanitize () {
        -e 's|"id": "[^"]*",|"id": "XXXXX",|g' \
        -e 's|"Date": "Fri, 05 Jan 2001 [^"]*0000"|"Date": "GENERATED_DATE"|g' \
        -e 's|"filename": "signature.asc",||g' \
+       -e 's|"duplicate": 1,||g' \
        -e 's|"filename": \["/[^"]*"\],|"filename": \["YYYYY"\],|g' \
        -e 's|"timestamp": 97.......|"timestamp": 42|g' \
        -e 's|"content-length": [1-9][0-9]*|"content-length": "NONZERO"|g'
 }
 
+notmuch_sexp_show_sanitize () {
+    sed \
+       -e 's|:id "[^"]*"|:id "XXXXX"|g' \
+       -e 's|:Date "Sat, 01 Jan 2000 [^"]*0000"|:Date "GENERATED_DATE"|g' \
+       -e 's|:filename "signature.asc"||g' \
+       -e 's|:duplicate 1 ||g' \
+       -e 's|:filename ("/[^"]*")|:filename ("YYYYY")|g' \
+       -e 's|:timestamp 9........|:timestamp 42|g' \
+       -e 's|:content-length [1-9][0-9]*|:content-length "NONZERO"|g'
+}
+
+notmuch_sexp_search_sanitize () {
+    sed -e 's|:thread "[^"]*"|:thread "XXX"|'
+}
+
 notmuch_emacs_error_sanitize () {
     local command
     command=$1
@@ -545,6 +561,10 @@ notmuch_date_sanitize () {
        -e 's/^Date: Fri, 05 Jan 2001 .*0000/Date: GENERATED_DATE/'
 }
 
+# remove redundant parts of notmuch-git internal paths
+notmuch_git_sanitize () {
+    sed  -e 's,tags/\([0-9a-f]\{2\}/\)\{2\},,' -e '/FORMAT/d'
+}
 notmuch_uuid_sanitize () {
     sed 's/[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}/UUID/g'
 }
@@ -872,6 +892,19 @@ test_C () {
     notmuch_dir_sanitize OUTPUT.stdout OUTPUT.stderr | notmuch_exception_sanitize | notmuch_debug_sanitize > OUTPUT
 }
 
+test_private_C () {
+    local exec_file test_file
+    exec_file="test${test_count}"
+    test_file="${exec_file}.c"
+    echo '#include <notmuch-private.h>' > ${test_file}
+    cat >> ${test_file}
+    ${TEST_CC} ${TEST_CFLAGS} -I${NOTMUCH_SRCDIR}/test -I${NOTMUCH_SRCDIR}/lib -I${NOTMUCH_SRCDIR}/util -I${NOTMUCH_SRCDIR}/compat ${NOTMUCH_GMIME_CFLAGS} -o ${exec_file} ${test_file} ${NOTMUCH_BUILDDIR}/lib/libnotmuch.a ${NOTMUCH_GMIME_LDFLAGS} ${NOTMUCH_XAPIAN_LDFLAGS} ${NOTMUCH_BUILDDIR}/util/libnotmuch_util.a ${NOTMUCH_SFSEXP_LDFLAGS} ${NOTMUCH_BUILDDIR}/parse-time-string/libparse-time-string.a -ltalloc -lstdc++
+    echo "== stdout ==" > OUTPUT.stdout
+    echo "== stderr ==" > OUTPUT.stderr
+    ./${exec_file} "$@" 1>>OUTPUT.stdout 2>>OUTPUT.stderr
+    notmuch_dir_sanitize OUTPUT.stdout OUTPUT.stderr | notmuch_exception_sanitize | notmuch_debug_sanitize > OUTPUT
+}
+
 make_shim () {
     local base_name test_file shim_file
     base_name="$1"
index 2037dfa6c43dd66de1007cff562b14ebed056c1a..c128d4d9e49568bf12a01322b7981ebbfdb2c058 100644 (file)
@@ -1 +1 @@
-0.36
+0.37