From: David Bremner Date: Mon, 10 Oct 2022 15:56:18 +0000 (-0300) Subject: Merge tag 'debian/0.37-1' into debian/bullseye-backports X-Git-Tag: archive/debian/0.37-1_bpo11+1~1 X-Git-Url: https://git.notmuchmail.org/git?a=commitdiff_plain;h=0558c46f78e3abc4d9ee27cd135b29ddf356073d;hp=ceeeebec2fdc97f8d323a3a7080da1be9e8e7f24;p=notmuch Merge tag 'debian/0.37-1' into debian/bullseye-backports notmuch release 0.37-1 for unstable (sid) [dgit] [dgit distro=debian no-split --quilt=linear] --- diff --git a/.gitignore b/.gitignore index f846ebec..f94d1480 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ /bindings/python-cffi/build/ /lib/libnotmuch*.dylib /lib/libnotmuch.so* +/nmbug /notmuch +/notmuch-git /notmuch-shared /releases /sh.config diff --git a/Makefile.local b/Makefile.local index d8bbf3e1..7699c208 100644 --- a/Makefile.local +++ b/Makefile.local @@ -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 c18d63d3..b83551f1 100644 --- 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) ========================= diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index 349bb79d..65d7dcb6 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -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 { diff --git a/bindings/python-cffi/notmuch2/_message.py b/bindings/python-cffi/notmuch2/_message.py index aa1cb875..d4b34e91 100644 --- a/bindings/python-cffi/notmuch2/_message.py +++ b/bindings/python-cffi/notmuch2/_message.py @@ -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) diff --git a/bindings/python-cffi/tests/conftest.py b/bindings/python-cffi/tests/conftest.py index 6835fd30..fe90c787 100644 --- a/bindings/python-cffi/tests/conftest.py +++ b/bindings/python-cffi/tests/conftest.py @@ -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 diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py index 59144626..943f335c 100644 --- a/bindings/python/notmuch/version.py +++ b/bindings/python/notmuch/version.py @@ -1,3 +1,3 @@ # this file should be kept in sync with ../../../version -__VERSION__ = '0.36' +__VERSION__ = '0.37' SOVERSION = '5' diff --git a/configure b/configure index 2f6d8b68..5247e05a 100755 --- 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 < 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 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 Sun, 14 Aug 2022 10:55:24 -0300 + +notmuch (0.37~rc0-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Sun, 14 Aug 2022 07:28:22 -0300 + notmuch (0.36-1~bpo11+1) bullseye-backports; urgency=medium * Rebuild for bullseye-backports. diff --git a/debian/control b/debian/control index a11d4130..2dcb8cc7 100644 --- a/debian/control +++ b/debian/control @@ -19,7 +19,9 @@ Build-Depends: dpkg-dev (>= 1.17.14), dtach (>= 0.8) , 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] , + git , gnupg , gpgsm , 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 index 00000000..2be08276 --- /dev/null +++ b/debian/notmuch-git.install @@ -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 index 00000000..e0895c86 --- /dev/null +++ b/debian/notmuch-git.manpages @@ -0,0 +1,2 @@ +usr/share/man/man1/notmuch-git.1.gz +usr/share/man/man1/nmbug.1.gz diff --git a/devel/emacs-keybindings.org b/devel/emacs-keybindings.org index 00977bc3..ad7f72ef 100644 --- a/devel/emacs-keybindings.org +++ b/devel/emacs-keybindings.org @@ -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 index 043c1863..00000000 --- a/devel/nmbug/nmbug +++ /dev/null @@ -1,852 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2011-2014 David Bremner -# W. Trevor King -# -# 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[^/]*)/(?P[^/]*)') - -# 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..repository, likely 'origin', and - branch..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 ... --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 , 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..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 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 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) diff --git a/devel/schemata b/devel/schemata index 01810888..66bcdbed 100644 --- a/devel/schemata +++ b/devel/schemata @@ -83,6 +83,7 @@ message = { headers: headers, crypto: crypto, + duplicate: integer, body?: [part] # omitted if --body=false } diff --git a/doc/Makefile.local b/doc/Makefile.local index d43ef269..51c729cf 100644 --- a/doc/Makefile.local +++ b/doc/Makefile.local @@ -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 index 00000000..543a5f9e --- /dev/null +++ b/doc/command-line.rst @@ -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 + notmuch-setup + diff --git a/doc/conf.py b/doc/conf.py index e46e1d4e..e23cb7d7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -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 index 00000000..1b0392e6 --- /dev/null +++ b/doc/elisp.py @@ -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 . + + +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} diff --git a/doc/index.rst b/doc/index.rst index fbdcf779..fec3e6c2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -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` diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst index acc5ecec..388315f6 100644 --- a/doc/man1/notmuch-config.rst +++ b/doc/man1/notmuch-config.rst @@ -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. +.. nmconfig:: built_with. + Compile time feature . 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. +.. nmconfig:: index.header. + Define the query 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. 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. +.. nmconfig:: query. + Expansion for named query called . 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. +.. nmconfig:: squery. + Expansion for named query called , 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 `). + (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 index 00000000..ac1908b6 --- /dev/null +++ b/doc/man1/notmuch-git.rst @@ -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 , --git-dir + + Operate on git repository *repo*. See :ref:`repo_location` for + defaults. + +.. option:: -p , --tag-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 , --log-level + + Log verbosity, one of: `critical`, `error`, `warning`, `info`, + `debug`. Defaults to `warning`. + +SUBCOMMANDS +----------- + +For help on a particular subcommand, run: 'notmuch-git ... --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 + +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..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..repository`, likely `origin`, and `branch..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 ` 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 `, 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)` diff --git a/doc/man1/notmuch-insert.rst b/doc/man1/notmuch-insert.rst index fe2bf26b..e05bd0b5 100644 --- a/doc/man1/notmuch-insert.rst +++ b/doc/man1/notmuch-insert.rst @@ -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 `, 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 ` 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= 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 ` 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 ` and :ref:`index.header -`. Tagging -is controlled by :ref:`new.tags ` and -:ref:`maildir.synchronize_flags `. See +:nmconfig:`index.decrypt` and :nmconfig:`index.header.\`. Tagging +is controlled by options :nmconfig:`new.tags` and +:nmconfig:`maildir.synchronize_flags`. See :any:`notmuch-config(1)` for details. EXIT STATUS diff --git a/doc/man1/notmuch-new.rst b/doc/man1/notmuch-new.rst index 398f8813..f0ed8eb8 100644 --- a/doc/man1/notmuch-new.rst +++ b/doc/man1/notmuch-new.rst @@ -82,10 +82,10 @@ CONFIGURATION ============= Indexing is influenced by the configuration options -:ref:`index.decrypt `, :ref:`index.header -`, and :ref:`new.ignore `. Tagging -is controlled by :ref:`new.tags ` and -:ref:`maildir.synchronize_flags `. See +:nmconfig:`index.decrypt`, :nmconfig:`index.header.\` +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 diff --git a/doc/man1/notmuch-reply.rst b/doc/man1/notmuch-reply.rst index fa0371f9..4f39a959 100644 --- a/doc/man1/notmuch-reply.rst +++ b/doc/man1/notmuch-reply.rst @@ -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 `. + .. option:: --format=(default|json|sexp|headers-only) default diff --git a/doc/man1/notmuch-show.rst b/doc/man1/notmuch-show.rst index 1b02d407..2c0a0de6 100644 --- a/doc/man1/notmuch-show.rst +++ b/doc/man1/notmuch-show.rst @@ -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 ` + .. 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 `. See +option :nmconfig:`show.extra_headers`. See :any:`notmuch-config(1)` for details. EXIT STATUS diff --git a/doc/man1/notmuch.rst b/doc/man1/notmuch.rst index c6ee3036..c488f12a 100644 --- a/doc/man1/notmuch.rst +++ b/doc/man1/notmuch.rst @@ -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-** 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 =========== diff --git a/doc/man7/notmuch-sexp-queries.rst b/doc/man7/notmuch-sexp-queries.rst index bc8e5086..d28f40bb 100644 --- a/doc/man7/notmuch-sexp-queries.rst +++ b/doc/man7/notmuch-sexp-queries.rst @@ -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: diff --git a/doc/notmuch-emacs.rst b/doc/notmuch-emacs.rst index 41f62390..846f5e67 100644 --- a/doc/notmuch-emacs.rst +++ b/doc/notmuch-emacs.rst @@ -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 ```` | 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 + + A lower bound on the number of characters that will + be used to display each column. + + .. describe:: float + 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 -------------------------- -```` +.. el:define-key:: + Move to the next widget (button or text entry field) -```` +.. el:define-key:: + Move to the previous widget. -```` +.. el:define-key:: + 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 ```` on the appropriate line. -``n,C-n,`` +.. el:define-key:: n + C-n + + Move to next line -``p,C-p,`` +.. el:define-key:: + p + C-p + + Move to previous line -```` +.. el:define-key:: + 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. -```` +.. el:define-key:: + 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 (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` -```` +.. el:define-key:: + 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 index 00000000..b76e71e0 --- /dev/null +++ b/doc/queries.rst @@ -0,0 +1,9 @@ +Notmuch Queries +=============== + +.. toctree:: + :titlesonly: + + man7/notmuch-search-terms + man7/notmuch-sexp-queries + man7/notmuch-properties diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index 6fc71cc7..84ba8c5e 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -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) diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index c679373b..ac878a61 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -21,7 +21,10 @@ ;;; 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 diff --git a/emacs/notmuch-query.el b/emacs/notmuch-query.el index 5c7f4f8d..2a46144c 100644 --- a/emacs/notmuch-query.el +++ b/emacs/notmuch-query.el @@ -25,17 +25,10 @@ ;;; 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) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index 7c1f02c9..2dcef981 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -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) diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el index 303c6fad..f63ac9a5 100644 --- a/emacs/notmuch-tree.el +++ b/emacs/notmuch-tree.el @@ -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 () diff --git a/emacs/notmuch.el b/emacs/notmuch.el index c9cf80dc..5cb7acd2 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -90,11 +90,11 @@ ("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 ec89b22e..0cf72e74 100644 --- 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 diff --git a/lib/database.cc b/lib/database.cc index df83e204..c05d70d3 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -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; diff --git a/lib/message.cc b/lib/message.cc index 63b216b6..1c87f8c0 100644 --- a/lib/message.cc +++ b/lib/message.cc @@ -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); diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h index 3cc79bc4..1d3d2b0c 100644 --- a/lib/notmuch-private.h +++ b/lib/notmuch-private.h @@ -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 diff --git a/lib/notmuch.h b/lib/notmuch.h index 2e6ec2af..0b0540b1 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -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. */ diff --git a/lib/parse-sexp.cc b/lib/parse-sexp.cc index 08fd7037..0f14d8b7 100644 --- a/lib/parse-sexp.cc +++ b/lib/parse-sexp.cc @@ -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) { diff --git a/lib/tags.c b/lib/tags.c index c7d3f66f..ec5366ff 100644 --- a/lib/tags.c +++ b/lib/tags.c @@ -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 * diff --git a/mime-node.c b/mime-node.c index d29c4e48..1c5d619b 100644 --- a/mime-node.c +++ b/mime-node.c @@ -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); diff --git a/notmuch-client.h b/notmuch-client.h index 9f57ac5e..21b49908 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -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 index 00000000..ceb86fbc --- /dev/null +++ b/notmuch-git.py @@ -0,0 +1,1218 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2011-2014 David Bremner +# W. Trevor King +# +# 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[^/]*)/(?P[^/]*)'), + _re.compile(_TAG_DIRECTORY + '([0-9a-f]{2}/){2}(?P[^/]*)/(?P[^/]*)')) + +# 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..repository, likely 'origin', and + branch..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 ... --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 , 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..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 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 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) diff --git a/notmuch-reply.c b/notmuch-reply.c index 9fca22db..44297251 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -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, ¶ms->crypto, &node)) + if (mime_node_open (notmuch, message, params->duplicate, ¶ms->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 = ¶ms.duplicate, .name = "duplicate" }, { .opt_inherit = notmuch_shared_options }, { } }; diff --git a/notmuch-show.c b/notmuch-show.c index 6a54d9c1..ee9efa74 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -23,6 +23,21 @@ #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 = ¶ms.crypto.verify, .name = "verify" }, { .opt_bool = ¶ms.output_body, .name = "body" }, { .opt_bool = ¶ms.include_html, .name = "include-html" }, + { .opt_int = ¶ms.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) diff --git a/notmuch.c b/notmuch.c index ac25ae18..37286b8f 100644 --- 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, ¬much, &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; diff --git a/performance-test/T06-emacs.sh b/performance-test/T06-emacs.sh index 66f0be58..c92bbd66 100755 --- a/performance-test/T06-emacs.sh +++ b/performance-test/T06-emacs.sh @@ -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 index 00000000..11dfec05 --- /dev/null +++ b/performance-test/T07-git.sh @@ -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 diff --git a/test/T055-path-config.sh b/test/T055-path-config.sh index 4897c814..58c824a2 100755 --- a/test/T055-path-config.sh +++ b/test/T055-path-config.sh @@ -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 < 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 diff --git a/test/T070-insert.sh b/test/T070-insert.sh index e1e3b151..7d5f842d 100755 --- a/test/T070-insert.sh +++ b/test/T070-insert.sh @@ -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"], diff --git a/test/T081-sexpr-search.sh b/test/T081-sexpr-search.sh index da819190..ce6b11b6 100755 --- a/test/T081-sexpr-search.sh +++ b/test/T081-sexpr-search.sh @@ -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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 diff --git a/test/T160-json.sh b/test/T160-json.sh index ec7b1461..4a797f6a 100755 --- a/test/T160-json.sh +++ b/test/T160-json.sh @@ -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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"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 \", \"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 < EXPECTED [ { "date_relative": "2001-01-05", + "duplicate": 1, "excluded": false, "filename": [ "${MAIL_DIR}/copy1", @@ -132,6 +133,7 @@ cat < EXPECTED [ { "date_relative": "2001-01-05", + "duplicate": 1, "excluded": false, "filename": "${MAIL_DIR}/copy1", "headers": { diff --git a/test/T170-sexp.sh b/test/T170-sexp.sh index 0d32560c..0be94bd2 100755 --- a/test/T170-sexp.sh +++ b/test/T170-sexp.sh @@ -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 \" :To \"Notmuch Test Suite \" :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 \" :To \"Notmuch Test Suite \" :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 \" :To \"Notmuch Test Suite \" :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 \" :To \"Notmuch Test Suite \" :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 \" :To \"Notmuch Test Suite \" :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 \" :To \"Notmuch Test Suite \" :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 \" :To \"Notmuch Test Suite \" :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 \" :To \"Notmuch Test Suite \" :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 \" :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 \" :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]=\"\"" "[body]=\"extra-headers test\""\ @@ -54,10 +54,9 @@ add_message "[subject]=\"extra-headers\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0 for ; 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 < 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 " :To "Notmuch Test Suite " :Date "Sat, 01 Jan 2000 12:00:00 +0000" :In-Reply-To "" :Received "from mail.example.com (mail.example.com [1.1.1.1])\011by mail.notmuchmail.org (some MTA) with ESMTP id 12345678\011for ; 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 " :To "Notmuch Test Suite " :Date "GENERATED_DATE" :In-Reply-To "" :Received "from mail.example.com (mail.example.com [1.1.1.1])\011by mail.notmuchmail.org (some MTA) with ESMTP id 12345678\011for ; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)")) ()))) EOF test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T190-multipart.sh b/test/T190-multipart.sh index d3b7f87c..cfe48ac5 100755 --- a/test/T190-multipart.sh +++ b/test/T190-multipart.sh @@ -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 <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 ", "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 ", "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 ", "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 ", "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 <", "Subject": "html message", "To": "B "}, @@ -742,8 +742,8 @@ EOF cat_expected_head > EXPECTED.nohtml cat <> 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 <> 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" diff --git a/test/T210-raw.sh b/test/T210-raw.sh index e1d50bf9..44082028 100755 --- a/test/T210-raw.sh +++ b/test/T210-raw.sh @@ -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 diff --git a/test/T220-reply.sh b/test/T220-reply.sh index 4e9984d2..207f5788 100755 --- a/test/T220-reply.sh +++ b/test/T220-reply.sh @@ -298,7 +298,8 @@ On Tue, 05 Jan 2010 15:43:56 -0000, ☃ 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 ", "Subject": "\u00e0\u00df\u00e7", "To": "Notmuch Test Suite " }, - "id": "'${gen_msg_id}'", + "id": "XXXXX", "match": false, "tags": [ "inbox", @@ -352,4 +353,40 @@ On Thu, 16 Jun 2016 22:14:41 -0400, Alice 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 diff --git a/test/T400-hooks.sh b/test/T400-hooks.sh index 0c84b7dd..35bf70c0 100755 --- a/test/T400-hooks.sh +++ b/test/T400-hooks.sh @@ -15,6 +15,15 @@ EOF echo "${TOKEN}" > ${2} } +create_printenv_hook () { + mkdir -p ${HOOK_DIR} + cat <"${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 < 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 < 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 index 00000000..0e1d9646 --- /dev/null +++ b/test/T405-external.sh @@ -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 <"${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 <"${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 < 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 < EXPECTED +${NOTMUCH_CONFIG}.alternate +EOF +notmuch --config "${NOTMUCH_CONFIG}.alternate" printenv +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_done diff --git a/test/T450-emacs-show.sh b/test/T450-emacs-show.sh index 057ad37e..559df8aa 100755 --- a/test/T450-emacs-show.sh +++ b/test/T450-emacs-show.sh @@ -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_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 < EXPECTED +Antoine Beaupré (2018-03-19) (attachment inbox) +Subject: Re: bug: "no top level messages" crash on Zen email loops +To: David Bremner , 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 < EXPECTED +Antoine Beaupré (2018-03-19) (attachment inbox) +Subject: Re: bug: "no top level messages" crash on Zen email loops +To: David Bremner , 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 < 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 < 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 index 00000000..0a27d066 --- /dev/null +++ b/test/T453-emacs-reply.sh @@ -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 < EXPECTED +From: Notmuch Test Suite +To: Antoine Beaupré +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é 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 index 00000000..3a770177 --- /dev/null +++ b/test/T454-emacs-dont-reply-names.sh @@ -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 < EXPECTED +From: Notmuch Test Suite +To: Mikhail Gusarov +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 < EXPECTED-PRED +From: Notmuch Test Suite +To: Lars Kellogg-Stedman +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 < EXPECTED-NIL +From: Notmuch Test Suite +To: Lars Kellogg-Stedman , Mikhail Gusarov +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 diff --git a/test/T460-emacs-tree.sh b/test/T460-emacs-tree.sh index 0f23b418..3a1c449e 100755 --- a/test/T460-emacs-tree.sh +++ b/test/T460-emacs-tree.sh @@ -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 diff --git a/test/T465-emacs-unthreaded.sh b/test/T465-emacs-unthreaded.sh index e7bc1439..a3ff85fd 100755 --- a/test/T465-emacs-unthreaded.sh +++ b/test/T465-emacs-unthreaded.sh @@ -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 diff --git a/test/T520-show.sh b/test/T520-show.sh index 12bde6c7..c7b73a6d 100755 --- a/test/T520-show.sh +++ b/test/T520-show.sh @@ -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 diff --git a/test/T560-lib-error.sh b/test/T560-lib-error.sh index 1f4482cb..a2901ff6 100755 --- a/test/T560-lib-error.sh +++ b/test/T560-lib-error.sh @@ -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} diff --git a/test/T562-lib-database.sh b/test/T562-lib-database.sh index 2314efd2..fedfc9ed 100755 --- a/test/T562-lib-database.sh +++ b/test/T562-lib-database.sh @@ -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 < 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 < 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 diff --git a/test/T563-lib-directory.sh b/test/T563-lib-directory.sh index ebd7fcb2..4711fcdf 100755 --- a/test/T563-lib-directory.sh +++ b/test/T563-lib-directory.sh @@ -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 < 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 < 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 < 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 diff --git a/test/T564-lib-query.sh b/test/T564-lib-query.sh index ff1d4984..53a63bf6 100755 --- a/test/T564-lib-query.sh +++ b/test/T564-lib-query.sh @@ -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 index 00000000..2a59f8dd --- /dev/null +++ b/test/T565-lib-tags.sh @@ -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 < c_head +#include +#include +#include +#include +#include +#include + +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 diff --git a/test/T566-lib-message.sh b/test/T566-lib-message.sh index 8b61d182..511d56ca 100755 --- a/test/T566-lib-message.sh +++ b/test/T566-lib-message.sh @@ -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 < 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 < 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 < 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 < EXPECTED @@ -305,12 +307,126 @@ cat < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < EXPECTED diff --git a/test/T568-lib-thread.sh b/test/T568-lib-thread.sh index b45836cd..b4c24ca2 100755 --- a/test/T568-lib-thread.sh +++ b/test/T568-lib-thread.sh @@ -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); diff --git a/test/T570-revision-tracking.sh b/test/T570-revision-tracking.sh index a59e7c98..e1cc684d 100755 --- a/test/T570-revision-tracking.sh +++ b/test/T570-revision-tracking.sh @@ -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); diff --git a/test/T610-message-property.sh b/test/T610-message-property.sh index 4ec85474..2685f3b5 100755 --- a/test/T610-message-property.sh +++ b/test/T610-message-property.sh @@ -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"); diff --git a/test/T620-lock.sh b/test/T620-lock.sh index 8f4c380f..99cc7010 100755 --- a/test/T620-lock.sh +++ b/test/T620-lock.sh @@ -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)); diff --git a/test/T640-database-modified.sh b/test/T640-database-modified.sh index 636b20c7..2c3fa735 100755 --- a/test/T640-database-modified.sh +++ b/test/T640-database-modified.sh @@ -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)); diff --git a/test/T720-lib-lifetime.sh b/test/T720-lib-lifetime.sh index 3d94d4df..e5afeaa2 100755 --- a/test/T720-lib-lifetime.sh +++ b/test/T720-lib-lifetime.sh @@ -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 index 00000000..55cec78a --- /dev/null +++ b/test/T850-git.sh @@ -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 <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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 index 00000000..258a74d1 --- /dev/null +++ b/test/corpora/attachment/x-gtar-compressed.eml @@ -0,0 +1,136 @@ +Return-path: +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 ) + 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?= +To: David Bremner , 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 + +--=-=-=-- diff --git a/test/corpora/default/bar/baz/05:2, b/test/corpora/default/bar/baz/05:2, index 75b05fa4..ce174b52 100644 --- a/test/corpora/default/bar/baz/05:2, +++ b/test/corpora/default/bar/baz/05:2, @@ -63,7 +63,7 @@ and +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 ) + 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 ) + 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 ) + 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 ) + 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 ) + id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000 +Date: Wed, 28 Nov 2012 22:40:10 +0000 +From: Anton Gladky +To: 691896@bugs.debian.org, control@bugs.debian.org, + 691896-submitter@bugs.debian.org +Message-ID: +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 + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-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 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 index 00000000..dc0476e7 --- /dev/null +++ b/test/corpora/duplicate/msg-1-2:2, @@ -0,0 +1,113 @@ +Return-path: +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 ) + 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 ) + 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 ) + 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 ) 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 ) + id 1TdqKR-0001dH-UL; Wed, 28 Nov 2012 22:42:03 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Anton Gladky +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Science Maintainers + +X-Loop: owner@bugs.debian.org +Resent-Date: Wed, 28 Nov 2012 22:42:02 +0000 +Resent-Message-ID: +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 ) + 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 ) + 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 ) + id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000 +Date: Wed, 28 Nov 2012 22:40:10 +0000 +From: Anton Gladky +To: 691896@bugs.debian.org, control@bugs.debian.org, + 691896-submitter@bugs.debian.org +Message-ID: +X-PTS-Approved: Yes +Resent-Sender: Debian BTS +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 , 691896@bugs.debian.org +List-Id: Mailing list for maintainer discussions and BTS messages + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-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 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 index 00000000..e118d784 --- /dev/null +++ b/test/corpora/duplicate/msg-2-1:2, @@ -0,0 +1,43 @@ +From: David Bremner +To: Samuel Bronson , 695159@bugs.debian.org, Debian Bug Tracking System +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 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 index 00000000..a7549c47 --- /dev/null +++ b/test/corpora/duplicate/msg-2-2:2, @@ -0,0 +1,140 @@ +Return-path: +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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 , 695159@bugs.debian.org +Resent-From: David Bremner +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacsen team +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 25 Oct 2018 13:45:01 +0000 +Resent-Message-ID: +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 ) + 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 ) + 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 +To: Samuel Bronson , 695159@bugs.debian.org, Debian Bug Tracking System +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: +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 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 index 00000000..8bb6a8c2 --- /dev/null +++ b/test/corpora/duplicate/msg-3-1:2, @@ -0,0 +1,183 @@ +Return-path: +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 ) + 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 ) + 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 ) + 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 ) 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 ) + id 1ga32F-0005PP-9m; Thu, 20 Dec 2018 18:27:03 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:02 +0000 +Resent-Message-ID: +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 ) + 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: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +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 + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 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" + +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 index 00000000..292d515f --- /dev/null +++ b/test/corpora/duplicate/msg-3-2:2, @@ -0,0 +1,184 @@ +Return-path: +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 ) + 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 ) + 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 ) + 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 ) 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 ) + id 1ga32K-0005Sn-7B; Thu, 20 Dec 2018 18:27:08 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:06 +0000 +Resent-Message-ID: +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 ) + 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: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +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 + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 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" + +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 index 00000000..bff2473c --- /dev/null +++ b/test/corpora/duplicate/msg-3-3:2, @@ -0,0 +1,178 @@ +Return-path: +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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 ) + 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 , 916807@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:04 +0000 +Resent-Message-ID: +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 ) + 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: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +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: +X-Debian: tracker.debian.org +X-Debian-Package: haskell-mode +X-PTS-Package: haskell-mode +X-PTS-Keyword: bts +Precedence: list +List-Unsubscribe: +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 index 00000000..861719d1 --- /dev/null +++ b/test/corpora/duplicate/msg-3-4:2, @@ -0,0 +1,184 @@ +Return-path: +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 ) + 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 ) + 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 ) + 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 ) 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 ) + id 1ga32O-0005Te-Sa; Thu, 20 Dec 2018 18:27:12 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:11 +0000 +Resent-Message-ID: +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 ) + 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: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +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 + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 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" + +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 index 00000000..af26374d --- /dev/null +++ b/test/corpora/duplicate/msg-3-5:2, @@ -0,0 +1,179 @@ +Return-path: +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 ) + 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 ) + 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 ) 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 ) + 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 +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Science Maintainers + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:13 +0000 +Resent-Message-ID: +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 ) + 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: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +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 + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 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" + +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 index 00000000..836f77b1 --- /dev/null +++ b/test/emacs-reply.expected-output/notmuch-reply-duplicate-4 @@ -0,0 +1,21 @@ +From: Notmuch Test Suite +To: Sean Whitton , 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 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 index 00000000..8299519c --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-depth @@ -0,0 +1,44 @@ +Lars Kellogg-Stedman (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 (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 (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed (hidden) ] + Mikhail Gusarov (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 (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 (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 19:50:40 -0500 + + [ multipart/mixed (hidden) ] + Carl Worth (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 index 00000000..e7c376bb --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-depth-1 @@ -0,0 +1,119 @@ +Lars Kellogg-Stedman (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 (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 (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed (hidden) ] + Mikhail Gusarov (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 (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 (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 19:50:40 -0500 + + [ multipart/mixed (hidden) ] + Carl Worth (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 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 index 00000000..6bf49d81 --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-duplicate-4 @@ -0,0 +1,20 @@ +Sean Whitton (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 index 00000000..d646353e --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-height-0 @@ -0,0 +1,97 @@ +Lars Kellogg-Stedman (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 (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 (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed (hidden) ] + Mikhail Gusarov (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 (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 (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + 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 " (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 (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 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 index 00000000..e2951d2b --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-multipart-alternative @@ -0,0 +1,62 @@ +Alex Botero-Lowry (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 +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 (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 index 00000000..cdde467e --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-size @@ -0,0 +1,64 @@ +Lars Kellogg-Stedman (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 (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 (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + 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 (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 (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 (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + 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 (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 index 00000000..ec34612e --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-size-450 @@ -0,0 +1,89 @@ +Lars Kellogg-Stedman (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 (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 (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + 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 (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 (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 (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + 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 " (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 (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.expected-output/raw-message-cf0c4d-52ad0a b/test/emacs.expected-output/raw-message-cf0c4d-52ad0a index 75b05fa4..ce174b52 100644 --- a/test/emacs.expected-output/raw-message-cf0c4d-52ad0a +++ b/test/emacs.expected-output/raw-message-cf0c4d-52ad0a @@ -63,7 +63,7 @@ and OUTPUT } +test_private_C () { + local exec_file test_file + exec_file="test${test_count}" + test_file="${exec_file}.c" + echo '#include ' > ${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" diff --git a/version.txt b/version.txt index 2037dfa6..c128d4d9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.36 +0.37