]> git.cworth.org Git - notmuch/commitdiff
Merge tag 'debian/0.22-1' into jessie-backports
authorDavid Bremner <david@tethera.net>
Tue, 28 Jun 2016 14:48:30 +0000 (16:48 +0200)
committerDavid Bremner <david@tethera.net>
Tue, 28 Jun 2016 14:48:30 +0000 (16:48 +0200)
notmuch Debian 0.22-1 upload (same as 0.22)

104 files changed:
NEWS
bindings/Makefile.local
bindings/python/README
bindings/python/docs/source/filesystem.rst
bindings/python/docs/source/status_and_errors.rst
bindings/python/docs/source/threads.rst
bindings/python/notmuch/__init__.py
bindings/python/notmuch/compat.py
bindings/python/notmuch/database.py
bindings/python/notmuch/errors.py
bindings/python/notmuch/filenames.py
bindings/python/notmuch/globals.py
bindings/python/notmuch/query.py
bindings/python/notmuch/threads.py
bindings/python/notmuch/version.py
configure
crypto.c
debian/changelog
debian/control
devel/STYLE
devel/nmbug/doc/.gitignore [new file with mode: 0644]
devel/nmbug/doc/Makefile [new file with mode: 0644]
devel/nmbug/doc/conf.py [new file with mode: 0644]
devel/nmbug/doc/index.rst [new file with mode: 0644]
devel/nmbug/doc/man1/notmuch-report.1.rst [new file with mode: 0644]
devel/nmbug/doc/man5/notmuch-report.json.5.rst [new file with mode: 0644]
devel/nmbug/nmbug
devel/nmbug/nmbug-status [deleted file]
devel/nmbug/notmuch-report [new file with mode: 0755]
devel/nmbug/notmuch-report.json [new file with mode: 0644]
devel/nmbug/status-config.json [deleted file]
devel/release-checks.sh
devel/try-emacs-mua [new file with mode: 0755]
doc/conf.py
doc/man1/notmuch-reply.rst
emacs/Makefile.local
emacs/coolj.el
emacs/make-deps.el
emacs/notmuch-address.el
emacs/notmuch-company.el [new file with mode: 0644]
emacs/notmuch-crypto.el
emacs/notmuch-hello.el
emacs/notmuch-jump.el
emacs/notmuch-lib.el
emacs/notmuch-maildir-fcc.el
emacs/notmuch-message.el
emacs/notmuch-mua.el
emacs/notmuch-parser.el
emacs/notmuch-print.el
emacs/notmuch-query.el
emacs/notmuch-show.el
emacs/notmuch-tag.el
emacs/notmuch-tree.el
emacs/notmuch-version.el.tmpl
emacs/notmuch-wash.el
emacs/notmuch.el
lib/database.cc
lib/index.cc
lib/message.cc
lib/notmuch-private.h
lib/notmuch.h
lib/query.cc
notmuch-client.h
notmuch-emacs-mua
notmuch-new.c
notmuch-reply.c
notmuch-show.c
notmuch-tag.c
status.c
test/.gitignore
test/Makefile.local
test/README
test/T050-new.sh
test/T060-count.sh
test/T150-tagging.sh
test/T190-multipart.sh
test/T310-emacs.sh
test/T355-smime.sh [new file with mode: 0755]
test/T360-symbol-hiding.sh
test/T530-upgrade.sh
test/T560-lib-error.sh
test/T580-thread-search.sh [new file with mode: 0755]
test/T590-thread-breakage.sh [new file with mode: 0755]
test/atomicity.py
test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-off
test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on
test/emacs-show.expected-output/notmuch-show-indent-thread-content-off
test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-off
test/emacs.expected-output/notmuch-hello
test/emacs.expected-output/notmuch-hello-long-names
test/emacs.expected-output/notmuch-hello-no-saved-searches
test/emacs.expected-output/notmuch-hello-with-empty
test/emacs.expected-output/notmuch-show-thread-maildir-storage
test/emacs.expected-output/notmuch-show-thread-maildir-storage-with-fourfold-indentation
test/emacs.expected-output/notmuch-show-thread-maildir-storage-without-indentation
test/ghost-report.cc [new file with mode: 0644]
test/smime/README [new file with mode: 0644]
test/smime/key+cert.pem [new file with mode: 0644]
test/smime/test.crt [new file with mode: 0644]
test/test-lib-common.sh
test/test-lib.el
test/test-lib.sh
test/tree.expected-output/notmuch-tree-show-window
version

diff --git a/NEWS b/NEWS
index 6681699d3830ac8e88bda332aa08d46ab6829f13..c945c245cb81b7b17c9d90a90f5ec1ce2d9c7d66 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,136 @@
+Notmuch 0.22 (2016-04-26)
+=========================
+
+General
+-------
+
+Xapian 1.3 support
+
+  Notmuch should now build (and the test suite should pass) on recent
+  releases of Xapian 1.3.x. It has been tested with Xapian 1.3.5.
+
+Limited support for S/MIME messages
+
+  Signature verification is supported, but not decryption. S/MIME
+  signature creation and S/MIME encryption are supported via built-in
+  support in Emacs. S/MIME support is not extensively tested at this
+  time.
+
+Bug Fixes
+
+   Fix for threading bug involving deleting and re-adding
+   messages. Fix for case-sensitive content disposition headers. Fix
+   handling of 1 character directory names at top level.
+
+Command Line Interface
+----------------------
+
+`notmuch show` now supports verifying S/MIME signatures
+
+  This support relies on an appropriately configured `gpgsm`.
+
+Build System
+------------
+
+Drop dependency on "pkg-config emacs".
+
+Emacs Interface
+---------------
+
+Notmuch replies now include all parts shown in the show view
+
+  There are two main user visible changes. The first is that rfc822
+  parts are now included in replies.
+
+  The second change is that part headers are now included in the reply
+  buffer to provide visible separation of the parts. The choice of
+  which part headers to show is customizable via the variable
+  `notmuch-mua-reply-insert-header-p-function`.
+
+Filtering or Limiting messages is now bound to `l` in the search view
+
+  This binding now matches the analogous binding in show view.
+
+`F` forwards all open messages in a thread
+
+  When viewing a thread of messages, the new binding `F` can be used
+  to generate a new outgoing message which forwards all of the open
+  messages in the thread. This is analogous to the `f` binding, which
+  forwards only the current message.
+
+Preferred content type can be determined from the message content
+
+  More flexibility in choosing which sub-part of a
+  multipart/alternative part is initially shown is available by
+  setting `notmuch-multipart/alternative-discouraged` to a function
+  that returns a list of discouraged types. The function so specified
+  is passed the message as an argument and can examine the message
+  content to determine which content types should be discouraged. This
+  is in addition to the current capabilities (i.e. setting
+  `notmuch-multipart/alternative-discouraged` to a list of discouraged
+  types).
+
+When viewing a thread ("show" mode), queries that match no messages no
+longer generate empty buffers
+
+  Should an attempt be made to view the thread corresponding to a
+  query that matches no messages, a warning message is now displayed
+  and the terminal bell rung rather than displaying an empty buffer
+  (or, in some cases, displaying an empty buffer and throwing an
+  error). This also affects re-display of the current thread.
+
+Handle S/MIME signatures in emacs
+
+  The emacs interface is now capable making and verifying S/MIME
+  signatures.
+
+`notmuch-message-address-insinuate` is now a no-op
+
+  This reduces the amount of interference with non-notmuch uses of
+  message-mode.
+
+Address completion improvements
+
+  An external script is no longer needed for address completion; if
+  you previously configured one, customize the variable
+  `notmuch-address-command` to try the internal completion. If
+  `company-mode` is available, notmuch uses it by default for
+  interactive address completion.
+
+Test and experiment with the emacs MUA available in source tree
+
+  `./devel/try-emacs-mua` runs emacs and fills the window with
+  information how to try the MUA safely. Emacs is configured to use
+  the notmuch (lisp) files located in `./emacs` directory.
+
+Documentation
+-------------
+
+New `notmuch-report(1)` and `notmuch-report.json(5)` man pages
+describe `notmuch-report` and its JSON configuration file.  You can
+build these files by running `make` in the `devel/nmbug/doc`
+directory.
+
+notmuch-report
+--------------
+
+Renamed from `nmbug-status`.  This script generates reports based on
+notmuch queries, and doesn't really have anything to do with nmbug,
+except for sharing the `NMBGIT` environment variable.  The new name
+focuses on the script's action, instead of its historical association
+with the nmbug workflow.  This should make it more discoverable for
+users looking for generic notmuch reporting tools.
+
+The default configuration file name (extracted from the `config`
+branch of `NBMGIT` has changed from `status-config.json` to
+`notmuch-report.json` so it is more obviously associated with the
+report-generating script.  The configuration file also has a new
+`meta.message-url` setting, which is documented in
+`notmuch-report.json(5)`.
+
+`notmuch-report` now wraps query phrases in parentheses when and-ing
+them together, to avoid confusion about clause grouping.
+
 Notmuch 0.21 (2015-10-29)
 =========================
 
@@ -409,7 +542,7 @@ from the config file.  Use something like:
          ...
       },
       ...
-    },
+    }
 
 Python Bindings
 ---------------
index 4ecf839d7c18138281c7894ce7274b2f52700139..11d11d4bcbeb19d217f221f8d250be0d4e2cde32 100644 (file)
@@ -10,8 +10,6 @@ ifeq ($(HAVE_RUBY_DEV),1)
                LIBNOTMUCH="../../lib/$(LINKER_NAME)" \
                ruby extconf.rb --vendor
        $(MAKE) -C $(dir)/ruby
-else
-       @echo Missing dependency, skipping ruby bindings
 endif
 
 CLEAN += $(patsubst %,$(dir)/ruby/%, \
index e27ede2c268ed9c493b6b205ebb9e7e26921b288..b20ae071456a5205f1a99dce2f320cb45cfeb89a 100644 (file)
@@ -1,5 +1,5 @@
-notmuch -- The python interface to notmuch.so
-==============================================
+notmuch -- The python interface to notmuch
+==========================================
 
 This module makes the functionality of the notmuch library
 (`http://notmuchmail.org`_) available to python. Successful import of
@@ -10,78 +10,8 @@ If you have downloaded the full source tarball, you can create the
 documentation with sphinx installed, go to the docs directory and
 "make html". A static version of the documentation is available at:
 
-http://packages.python.org/notmuch/
+  https://notmuch.readthedocs.org/projects/notmuch-python/
 
-The current source code is being hosted at
-http://bitbucket.org/spaetz/cnotmuch which also provides an issue
-tracker, and release downloads. This package is tracked by the python
-package index repository at `http://pypi.python.org/pypi/notmuch`_ and can thus be installed on a user's computer easily via "sudo easy_install notmuch" (you will still need to install the notmuch shared library separately as it is not included in this package).
+To build the python bindings, do
 
-The original source has been provided by (c)Sebastian Spaeth, 2010.
-All code is available under the GNU GPLv3+ (see docs/COPYING) unless specified otherwise.
-
-
-INSTALLATION & DEINSTALL
-------------------------
-
-The notmuch python module is available on pypi.python.org. This means
-you can do "easy_install notmuch" on your linux box and it will get
-installed into:
-
-/usr/local/lib/python2.x/dist-packages/
-
-For uninstalling, you'll need to remove the "notmuch-0.4-py2.x.egg"
-(or similar) directory and delete one entry in the "easy-install.pth"
-file in that directory.
-
-It needs to have a libnotmuch.so or libnotmuch.so.1 available in some
-library folder or will raise an exception when loading.
-"OSError: libnotmuch.so.1: cannot open shared object file: No such file or directory"
-
-
-Usage
------
-For more examples of how to use the notmuch interface, have a look at the
-notmuch "binary" and the generated documentation.
-
-Example session:
->>>import notmuch
->>>db = notmuch.Database("/home/spaetz/mail")
-db.get_path()
-'/home/spaetz/mail'
->>>tags = db.get_all_tags()
->>>for tag in tags: 
->>>  print tag
-inbox
-...
-maildir::draft
-#---------------------------------------------
-
-q = notmuch.Query(db,'from:Sebastian')
-count = len(q.search_messages())
-1300
-
-#---------------------------------------------
-
->>>db = notmuch.Database("/home/spaetz/mailHAHA")
-NotmuchError: Could not open the specified database
-
-#---------------------------------------------
-
->>>tags = notmuch.Database("/home/spaetz/mail").get_all_tags()
->>>del(tags)
-
-
-Building for a Debian package
-------------------------------
-dpkg-buildpackage -i"\.hg|\/build"
-
-
-Changelog
----------
-0.1   First public release
-0.1.1 Fixed Database.create_query()
-0.2.0 Implemented Thread() and Threads() methods
-0.2.1 Implemented the remaining API methods, notably Directory() and Filenames()
-0.2.2 Bug fixes
-0.3.0 Incorporated in the notmuchmail.org git repository
\ No newline at end of file
+  python setup.py install --prefix=path/to/your/preferred/location
index 4eb781077940046a3e57e50d9f0969cb44806dd4..a23ae41a875685214c45a57bfb41720e29f3538f 100644 (file)
@@ -8,7 +8,11 @@ Files and directories
 
 .. autoclass:: Filenames
 
-   .. automethod:: Filenames.__len__
+   .. method:: Filenames.__len__
+   .. warning::
+      :meth:`__len__` was removed in version 0.22 as it exhausted the
+      iterator and broke list(Filenames()). Use `len(list(names))`
+      instead.
 
 :class:`Directoy` -- A directory entry in the database
 ------------------------------------------------------
index dd6e31f802a28e651e03efb137d173df93466a60..68913f160beb06166cd58c1e2c0448613c987dff 100644 (file)
@@ -47,5 +47,11 @@ The following exceptions are all directly derived from NotmuchError. Each of the
    :members:
 .. autoexception:: UnbalancedAtomicError(message=None)
    :members:
+.. autoexception:: UnsupportedOperationError(message=None)
+   :members:
+.. autoexception:: UpgradeRequiredError(message=None)
+   :members:
+.. autoexception:: PathError(message=None)
+   :members:
 .. autoexception:: NotInitializedError(message=None)
    :members:
index e5a8c8a95d1861ba755c34e7cd7c643a13836aaa..4324ac82a389611d8a633c65f015dabb7c4d4aa0 100644 (file)
@@ -5,6 +5,10 @@
 
 .. autoclass:: Threads
 
-   .. automethod:: __len__
+   .. method:: __len__
+   .. warning::
+      :meth:`__len__` was removed in version 0.22 as it exhausted the
+      iterator and broke list(Threads()). Use `len(list(msgs))`
+      instead.
 
-   .. automethod:: __str__
+.. automethod:: __str__
index 5561624e11d72b365854588d6fab6a71d1a7d1bd..29416a5b25bc014716b55d55771370065e2c1252 100644 (file)
@@ -75,6 +75,9 @@ from .errors import (
     UnbalancedFreezeThawError,
     UnbalancedAtomicError,
     NotInitializedError,
+    UnsupportedOperationError,
+    UpgradeRequiredError,
+    PathError,
 )
 from .version import __VERSION__
 __LICENSE__ = "GPL v3+"
index adc8d244e4f251a5c5d3edd4cae78d7a80dc695b..daa268c12cfb2de49a796d2f70c074498c220ff8 100644 (file)
@@ -65,3 +65,7 @@ else:
             raise TypeError('Expected str, got %s' % type(value))
 
         return value.encode('utf-8', 'replace')
+
+# We import the SafeConfigParser class on behalf of other code to cope
+# with the differences between Python 2 and 3.
+SafeConfigParser # avoid warning about unused import
index 5b58e099b1fde6d692f047d0e32c4ccd9ae0493f..f30453345e0dabd6a1cb4fa81596c50bdac65143 100644 (file)
@@ -36,7 +36,6 @@ from .errors import (
     NotmuchError,
     NullPointerError,
     NotInitializedError,
-    ReadOnlyDatabaseError,
 )
 from .message import Message
 from .tag import Tags
@@ -484,7 +483,10 @@ class Database(object):
                removed.
         """
         self._assert_db_is_initialized()
-        return self._remove_message(self._db, _str(filename))
+        status = self._remove_message(self._db, _str(filename))
+        if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
+            raise NotmuchError(status)
+        return status
 
     def find_message(self, msgid):
         """Returns a :class:`Message` as identified by its message ID
@@ -575,6 +577,22 @@ class Database(object):
         """
         return Query(self, querystring)
 
+    """notmuch_database_status_string"""
+    _status_string = nmlib.notmuch_database_status_string
+    _status_string.argtypes = [NotmuchDatabaseP]
+    _status_string.restype = c_char_p
+
+    def status_string(self):
+        """Returns the status string of the database
+
+        This is sometimes used for additional error reporting
+        """
+        self._assert_db_is_initialized()
+        s = Database._status_string(self._db)
+        if s:
+            return s.decode('utf-8', 'ignore')
+        return s
+
     def __repr__(self):
         return "'Notmuch DB " + self.get_path() + "'"
 
index f153a9c5bcf5c8c9c8e6ee05600eb4ffe998dd82..abca51d738516226a1fce04366f54cb8b362cd83 100644 (file)
@@ -56,6 +56,9 @@ STATUS = Status(['SUCCESS',
   'TAG_TOO_LONG',
   'UNBALANCED_FREEZE_THAW',
   'UNBALANCED_ATOMIC',
+  'UNSUPPORTED_OPERATION',
+  'UPGRADE_REQUIRED',
+  'PATH_ERROR',
   'NOT_INITIALIZED'])
 """STATUS is a class, whose attributes provide constants that serve as return
 indicators for notmuch functions. Currently the following ones are defined. For
@@ -73,6 +76,9 @@ description.
   * TAG_TOO_LONG
   * UNBALANCED_FREEZE_THAW
   * UNBALANCED_ATOMIC
+  * UNSUPPORTED_OPERATION
+  * UPGRADE_REQUIRED
+  * PATH_ERROR
   * NOT_INITIALIZED
 
 Invoke the class method `notmuch.STATUS.status2str` with a status value as
@@ -101,6 +107,9 @@ class NotmuchError(Exception, Python3StringMixIn):
             STATUS.TAG_TOO_LONG: TagTooLongError,
             STATUS.UNBALANCED_FREEZE_THAW: UnbalancedFreezeThawError,
             STATUS.UNBALANCED_ATOMIC: UnbalancedAtomicError,
+            STATUS.UNSUPPORTED_OPERATION: UnsupportedOperationError,
+            STATUS.UPGRADE_REQUIRED: UpgradeRequiredError,
+            STATUS.PATH_ERROR: PathError,
             STATUS.NOT_INITIALIZED: NotInitializedError,
         }
         assert 0 < status <= len(subclasses)
@@ -175,6 +184,18 @@ class UnbalancedAtomicError(NotmuchError):
     status = STATUS.UNBALANCED_ATOMIC
 
 
+class UnsupportedOperationError(NotmuchError):
+    status = STATUS.UNSUPPORTED_OPERATION
+
+
+class UpgradeRequiredError(NotmuchError):
+    status = STATUS.UPGRADE_REQUIRED
+
+
+class PathError(NotmuchError):
+    status = STATUS.PATH_ERROR
+
+
 class NotInitializedError(NotmuchError):
     """Derived from NotmuchError, this occurs if the underlying data
     structure (e.g. database is not initialized (yet) or an iterator has
index 229f414de63224053b026a933a305d1ba9ad990b..f8f383e4fd5370d2ad3b801ec36ae4ae427ee6c1 100644 (file)
@@ -19,7 +19,6 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
 from ctypes import c_char_p
 from .globals import (
     nmlib,
-    NotmuchMessageP,
     NotmuchFilenamesP,
     Python3StringMixIn,
 )
@@ -48,7 +47,7 @@ class Filenames(Python3StringMixIn):
 
         as well as::
 
-           number_of_names = len(names)
+           list_of_names = list(names)
 
         and even a simple::
 
@@ -123,28 +122,10 @@ class Filenames(Python3StringMixIn):
         return "\n".join(self)
 
     _destroy = nmlib.notmuch_filenames_destroy
-    _destroy.argtypes = [NotmuchMessageP]
+    _destroy.argtypes = [NotmuchFilenamesP]
     _destroy.restype = None
 
     def __del__(self):
         """Close and free the notmuch filenames"""
         if self._files_p:
             self._destroy(self._files_p)
-
-    def __len__(self):
-        """len(:class:`Filenames`) returns the number of contained files
-
-        .. note::
-
-            This method exhausts the iterator object, so you will not be able to
-            iterate over them again.
-        """
-        if not self._files_p:
-            raise NotInitializedError()
-
-        i = 0
-        while self._valid(self._files_p):
-            self._move_to_next(self._files_p)
-            i += 1
-        self._files_p = None
-        return i
index 70cfdc3d12f5d3d7b6e6a84208ec7c133926afa3..6872a2916a72b1fffa6c9df72c633440f48e2e57 100644 (file)
@@ -33,6 +33,11 @@ except:
 
 from .compat import Python3StringMixIn, encode_utf8 as _str
 
+# We import these on behalf of other modules.  Silence warning about
+# these symbols not being used.
+Python3StringMixIn
+_str
+
 class Enum(object):
     """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc..."""
     def __init__(self, names):
index 378aa47d05bb08522df30d0bfff63fab6d48265d..4327007223dcb0d5b52a317760bc988401e32244 100644 (file)
@@ -28,6 +28,7 @@ from .globals import (
     NotmuchMessagesP,
 )
 from .errors import (
+    NotmuchError,
     NullPointerError,
     NotInitializedError,
 )
@@ -133,10 +134,10 @@ class Query(object):
         self._assert_query_is_initialized()
         self._exclude_tag(self._query, _str(tagname))
 
-    """notmuch_query_search_threads"""
-    _search_threads = nmlib.notmuch_query_search_threads
-    _search_threads.argtypes = [NotmuchQueryP]
-    _search_threads.restype = NotmuchThreadsP
+    """notmuch_query_search_threads_st"""
+    _search_threads_st = nmlib.notmuch_query_search_threads_st
+    _search_threads_st.argtypes = [NotmuchQueryP, POINTER(NotmuchThreadsP)]
+    _search_threads_st.restype = c_uint
 
     def search_threads(self):
         """Execute a query for threads
@@ -153,16 +154,19 @@ class Query(object):
         :raises: :exc:`NullPointerError` if search_threads failed
         """
         self._assert_query_is_initialized()
-        threads_p = Query._search_threads(self._query)
+        threads_p = NotmuchThreadsP() # == NULL
+        status = Query._search_threads_st(self._query, byref(threads_p))
+        if status != 0:
+            raise NotmuchError(status)
 
         if not threads_p:
             raise NullPointerError
         return Threads(threads_p, self)
 
-    """notmuch_query_search_messages"""
-    _search_messages = nmlib.notmuch_query_search_messages
-    _search_messages.argtypes = [NotmuchQueryP]
-    _search_messages.restype = NotmuchMessagesP
+    """notmuch_query_search_messages_st"""
+    _search_messages_st = nmlib.notmuch_query_search_messages_st
+    _search_messages_st.argtypes = [NotmuchQueryP, POINTER(NotmuchMessagesP)]
+    _search_messages_st.restype = c_uint
 
     def search_messages(self):
         """Filter messages according to the query and return
@@ -172,7 +176,10 @@ class Query(object):
         :raises: :exc:`NullPointerError` if search_messages failed
         """
         self._assert_query_is_initialized()
-        msgs_p = Query._search_messages(self._query)
+        msgs_p = NotmuchMessagesP() # == NULL
+        status = Query._search_messages_st(self._query, byref(msgs_p))
+        if status != 0:
+            raise NotmuchError(status)
 
         if not msgs_p:
             raise NullPointerError
index f8ca34a9b14cc0f801a019848d4177a836cfc1a0..a550523fae8902636ddbb9c141ea53660158503f 100644 (file)
@@ -46,7 +46,7 @@ class Threads(Python3StringMixIn):
 
     as well as::
 
-       number_of_msgs = len(threads)
+       list_of_threads = list(threads)
 
     will "exhaust" the threads. If you need to re-iterate over a list of
     messages you will need to retrieve a new :class:`Threads` object.
@@ -64,8 +64,7 @@ class Threads(Python3StringMixIn):
       for thread in threads:
          threadlist.append(thread)
 
-      # threads is "exhausted" now and even len(threads) will raise an
-      # exception.
+      # threads is "exhausted" now.
       # However it will be kept around until all retrieved Thread() objects are
       # also deleted. If you did e.g. an explicit del(threads) here, the
       # following lines would fail.
@@ -132,30 +131,6 @@ class Threads(Python3StringMixIn):
         return thread
     next = __next__ # python2.x iterator protocol compatibility
 
-    def __len__(self):
-        """len(:class:`Threads`) returns the number of contained Threads
-
-        .. note:: As this iterates over the threads, we will not be able to
-               iterate over them again! So this will fail::
-
-                 #THIS FAILS
-                 threads = Database().create_query('').search_threads()
-                 if len(threads) > 0:              #this 'exhausts' threads
-                     # next line raises :exc:`NotInitializedError`!!!
-                     for thread in threads: print thread
-        """
-        if not self._threads:
-            raise NotInitializedError()
-
-        i = 0
-        # returns 'bool'. On out-of-memory it returns None
-        while self._valid(self._threads):
-            self._move_to_next(self._threads)
-            i += 1
-        # reset self._threads to mark as "exhausted"
-        self._threads = None
-        return i
-
     def __nonzero__(self):
         '''
         Implement truth value testing. If __nonzero__ is not
index db9d0764246b65d8444058bcae8cde5374dcb64d..c1d472bea1c1503651ec16912adef9a50107100d 100644 (file)
@@ -1,3 +1,3 @@
 # this file should be kept in sync with ../../../version
-__VERSION__ = '0.21'
+__VERSION__ = '0.22'
 SOVERSION = '4'
index 440d678c7eb3828041a24577cf7f1e37bf36c2ab..4fc31ccf8e79264b96ecb96d92794f295eb0205d 100755 (executable)
--- a/configure
+++ b/configure
@@ -51,7 +51,7 @@ CPPFLAGS=${CPPFLAGS:-}
 CXXFLAGS_for_sh=${CXXFLAGS:-${CFLAGS}}
 CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)}
 LDFLAGS=${LDFLAGS:-}
-XAPIAN_CONFIG=${XAPIAN_CONFIG:-xapian-config}
+XAPIAN_CONFIG=${XAPIAN_CONFIG:-}
 PYTHON=${PYTHON:-}
 
 # We don't allow the EMACS or GZIP Makefile variables inherit values
@@ -341,7 +341,7 @@ fi
 
 printf "Checking for Xapian development files... "
 have_xapian=0
-for xapian_config in ${XAPIAN_CONFIG}; do
+for xapian_config in ${XAPIAN_CONFIG} xapian-config xapian-config-1.3; do
     if ${xapian_config} --version > /dev/null 2>&1; then
        xapian_version=$(${xapian_config} --version | sed -e 's/.* //')
        printf "Yes (%s).\n" ${xapian_version}
@@ -371,7 +371,25 @@ if [ ${have_xapian} = "1" ]; then
     esac
 fi
 
-
+default_xapian_backend=""
+if [ ${have_xapian} = "1" ]; then
+    printf "Testing default Xapian backend... "
+    cat >_default_backend.cc <<EOF
+#include <xapian.h>
+int main(int argc, char** argv) {
+   Xapian::WritableDatabase db("test.db",Xapian::DB_CREATE_OR_OPEN);
+}
+EOF
+    ${CXX} ${CXXLAGS} ${xapian_cxxflags} _default_backend.cc -o _default_backend ${xapian_ldflags}
+    ./_default_backend
+    if [ -f test.db/iamglass ]; then
+       default_xapian_backend=glass
+    else
+       default_xapian_backend=chert
+    fi
+    printf "${default_xapian_backend}\n";
+    rm -rf test.db _default_backend _default_backend.cc
+fi
 # we need to have a version >= 2.6.5 to avoid a crypto bug. We need
 # 2.6.7 for permissive "From " header handling.
 GMIME_MINVER=2.6.7
@@ -472,19 +490,11 @@ else
 fi
 
 if [ -z "${EMACSLISPDIR}" ]; then
-    if pkg-config --exists emacs; then
-       EMACSLISPDIR=$(pkg-config emacs --variable sitepkglispdir)
-    else
-       EMACSLISPDIR='$(prefix)/share/emacs/site-lisp'
-    fi
+    EMACSLISPDIR='$(prefix)/share/emacs/site-lisp'
 fi
 
 if [ -z "${EMACSETCDIR}" ]; then
-    if pkg-config --exists emacs; then
-       EMACSETCDIR=$(pkg-config emacs --variable sitepkglispdir)
-    else
-       EMACSETCDIR='$(prefix)/share/emacs/site-lisp'
-    fi
+    EMACSETCDIR='$(prefix)/share/emacs/site-lisp'
 fi
 
 printf "Checking if emacs is available... "
@@ -977,6 +987,10 @@ HAVE_STRCASESTR = ${have_strcasestr}
 # build its own version)
 HAVE_STRSEP = ${have_strsep}
 
+# Whether the timegm function is available (if not, then notmuch will
+# build its own version)
+HAVE_TIMEGM = ${have_timegm}
+
 # Whether struct dirent has d_type (if not, then notmuch will use stat)
 HAVE_D_TYPE = ${have_d_type}
 
@@ -1005,6 +1019,9 @@ LINKER_RESOLVES_LIBRARY_DEPENDENCIES = ${linker_resolves_library_dependencies}
 XAPIAN_CXXFLAGS = ${xapian_cxxflags}
 XAPIAN_LDFLAGS = ${xapian_ldflags}
 
+# Which backend will Xapian use by default?
+DEFAULT_XAPIAN_BACKEND = ${default_xapian_backend}
+
 # Flags needed to compile and link against GMime
 GMIME_CFLAGS = ${gmime_cflags}
 GMIME_LDFLAGS = ${gmime_ldflags}
@@ -1049,6 +1066,7 @@ CONFIGURE_CFLAGS = -DHAVE_GETLINE=\$(HAVE_GETLINE) \$(GMIME_CFLAGS)      \\
                   \$(VALGRIND_CFLAGS)                                   \\
                   -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR)                 \\
                   -DHAVE_STRSEP=\$(HAVE_STRSEP)                         \\
+                  -DHAVE_TIMEGM=\$(HAVE_TIMEGM)                         \\
                   -DHAVE_D_TYPE=\$(HAVE_D_TYPE)                         \\
                   -DSTD_GETPWUID=\$(STD_GETPWUID)                       \\
                   -DSTD_ASCTIME=\$(STD_ASCTIME)                         \\
@@ -1062,6 +1080,7 @@ CONFIGURE_CXXFLAGS = -DHAVE_GETLINE=\$(HAVE_GETLINE) \$(GMIME_CFLAGS)    \\
                     \$(VALGRIND_CFLAGS) \$(XAPIAN_CXXFLAGS)             \\
                     -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR)               \\
                     -DHAVE_STRSEP=\$(HAVE_STRSEP)                       \\
+                    -DHAVE_TIMEGM=\$(HAVE_TIMEGM)                       \\
                     -DHAVE_D_TYPE=\$(HAVE_D_TYPE)                       \\
                     -DSTD_GETPWUID=\$(STD_GETPWUID)                     \\
                     -DSTD_ASCTIME=\$(STD_ASCTIME)                       \\
@@ -1079,6 +1098,9 @@ cat > sh.config <<EOF
 # Whether the Xapian version in use supports compaction
 NOTMUCH_HAVE_XAPIAN_COMPACT=${have_xapian_compact}
 
+# Which backend will Xapian use by default?
+NOTMUCH_DEFAULT_XAPIAN_BACKEND=${default_xapian_backend}
+
 # do we have man pages?
 NOTMUCH_HAVE_MAN=$((have_sphinx))
 
index a6eb27db7f630c131149377f7e51a190df86f613..3dabc97b4346f6304f2e0fbf925efabf87d03d8c 100644 (file)
--- a/crypto.c
+++ b/crypto.c
 
 /* Create a GPG context (GMime 2.6) */
 static notmuch_crypto_context_t *
-create_gpg_context (const char *gpgpath)
+create_gpg_context (notmuch_crypto_t *crypto)
 {
     notmuch_crypto_context_t *gpgctx;
 
+    if (crypto->gpgctx)
+       return crypto->gpgctx;
+
     /* TODO: GMimePasswordRequestFunc */
-    gpgctx = g_mime_gpg_context_new (NULL, gpgpath ? gpgpath : "gpg");
-    if (! gpgctx)
+    gpgctx = g_mime_gpg_context_new (NULL, crypto->gpgpath ? crypto->gpgpath : "gpg");
+    if (! gpgctx) {
+       fprintf (stderr, "Failed to construct gpg context.\n");
        return NULL;
+    }
+    crypto->gpgctx = gpgctx;
 
     g_mime_gpg_context_set_use_agent ((GMimeGpgContext *) gpgctx, TRUE);
     g_mime_gpg_context_set_always_trust ((GMimeGpgContext *) gpgctx, FALSE);
@@ -37,12 +43,57 @@ create_gpg_context (const char *gpgpath)
     return gpgctx;
 }
 
+/* Create a PKCS7 context (GMime 2.6) */
+static notmuch_crypto_context_t *
+create_pkcs7_context (notmuch_crypto_t *crypto)
+{
+    notmuch_crypto_context_t *pkcs7ctx;
+
+    if (crypto->pkcs7ctx)
+       return crypto->pkcs7ctx;
+
+    /* TODO: GMimePasswordRequestFunc */
+    pkcs7ctx = g_mime_pkcs7_context_new (NULL);
+    if (! pkcs7ctx) {
+       fprintf (stderr, "Failed to construct pkcs7 context.\n");
+       return NULL;
+    }
+    crypto->pkcs7ctx = pkcs7ctx;
+
+    g_mime_pkcs7_context_set_always_trust ((GMimePkcs7Context *) pkcs7ctx,
+                                          FALSE);
+
+    return pkcs7ctx;
+}
+static const struct {
+    const char *protocol;
+    notmuch_crypto_context_t *(*get_context) (notmuch_crypto_t *crypto);
+} protocols[] = {
+    {
+       .protocol = "application/pgp-signature",
+       .get_context = create_gpg_context,
+    },
+    {
+       .protocol = "application/pgp-encrypted",
+       .get_context = create_gpg_context,
+    },
+    {
+       .protocol = "application/pkcs7-signature",
+       .get_context = create_pkcs7_context,
+    },
+    {
+       .protocol = "application/x-pkcs7-signature",
+       .get_context = create_pkcs7_context,
+    },
+};
+
 /* for the specified protocol return the context pointer (initializing
  * if needed) */
 notmuch_crypto_context_t *
 notmuch_crypto_get_context (notmuch_crypto_t *crypto, const char *protocol)
 {
     notmuch_crypto_context_t *cryptoctx = NULL;
+    size_t i;
 
     if (! protocol) {
        fprintf (stderr, "Cryptographic protocol is empty.\n");
@@ -55,19 +106,15 @@ notmuch_crypto_get_context (notmuch_crypto_t *crypto, const char *protocol)
      * parameter names as defined in this document are
      * case-insensitive."  Thus, we use strcasecmp for the protocol.
      */
-    if (strcasecmp (protocol, "application/pgp-signature") == 0 ||
-       strcasecmp (protocol, "application/pgp-encrypted") == 0) {
-       if (! crypto->gpgctx) {
-           crypto->gpgctx = create_gpg_context (crypto->gpgpath);
-           if (! crypto->gpgctx)
-               fprintf (stderr, "Failed to construct gpg context.\n");
-       }
-       cryptoctx = crypto->gpgctx;
-    } else {
-       fprintf (stderr, "Unknown or unsupported cryptographic protocol.\n");
+    for (i = 0; i < ARRAY_SIZE (protocols); i++) {
+       if (strcasecmp (protocol, protocols[i].protocol) == 0)
+           return protocols[i].get_context (crypto);
     }
 
-    return cryptoctx;
+    fprintf (stderr, "Unknown or unsupported cryptographic protocol %s.\n",
+            protocol);
+
+    return NULL;
 }
 
 int
@@ -78,5 +125,10 @@ notmuch_crypto_cleanup (notmuch_crypto_t *crypto)
        crypto->gpgctx = NULL;
     }
 
+    if (crypto->pkcs7ctx) {
+       g_object_unref (crypto->pkcs7ctx);
+       crypto->pkcs7ctx = NULL;
+    }
+
     return 0;
 }
index 16ad765a0da2ea66bcc4c197710aee2a4bc5de48..6faabd0b7a01b514583bf9fffef7c2c74de42805 100644 (file)
@@ -1,3 +1,22 @@
+notmuch (0.22-1) unstable; urgency=medium
+
+  * New upstream release.  See /usr/share/doc/notmuch/NEWS for new
+    features and bug fixes.
+
+ -- David Bremner <bremner@debian.org>  Tue, 26 Apr 2016 21:31:44 -0300
+
+notmuch (0.22~rc1-1) experimental; urgency=medium
+
+  * Upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sun, 24 Apr 2016 18:03:15 -0300
+
+notmuch (0.22~rc0-1) experimental; urgency=medium
+
+  * Upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sat, 16 Apr 2016 08:45:32 -0300
+
 notmuch (0.21-3~bpo8+1) jessie-backports; urgency=medium
 
   * Rebuild for jessie-backports.
index 7e6a548c9948577054f9d7f2875322b0b2f6c039..a46e391b15ff90e7a0b0dfa644e99c7cdabbd33c 100644 (file)
@@ -7,6 +7,7 @@ Uploaders:
  David Bremner <bremner@debian.org>
 Build-Conflicts: ruby1.8, gdb-minimal, gdb [s390x ia64 armel ppc64el mips mipsel mips64el]
 Build-Depends:
+ dpkg-dev (>= 1.17.14),
  debhelper (>= 9),
  pkg-config,
  libxapian-dev,
@@ -22,6 +23,7 @@ Build-Depends:
  emacs23-nox | emacs23 (>=23~) | emacs23-lucid (>=23~),
  gdb [!s390x !ia64 !armel !ppc64el !mips !mipsel !mips64el],
  dtach (>= 0.8),
+ gpgsm <!nocheck>,
  bash-completion (>=1.9.0~)
 Standards-Version: 3.9.6
 Homepage: http://notmuchmail.org/
@@ -31,7 +33,7 @@ Vcs-Browser: http://git.notmuchmail.org/git/notmuch
 Package: notmuch
 Architecture: any
 Depends: libnotmuch4 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends}
-Recommends: notmuch-emacs | notmuch-vim | notmuch-mutt | alot,  gnupg-agent
+Recommends: notmuch-emacs | notmuch-vim | notmuch-mutt | alot,  gnupg-agent, gpgsm
 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
index 24bd5482a4899b185f923302bb4801177dc6b3ea..da6531248feb7e827c6f5066faaf1049a61a2652 100644 (file)
@@ -25,9 +25,7 @@ The following nonsense code demonstrates many aspects of the style:
 static some_type
 function (param_type param, param_type param)
 {
-   int i;
-
-   for (i = 0; i < 10; i++) {
+   for (int i = 0; i < 10; i++) {
        int j;
 
        j = i + 10;
@@ -64,12 +62,19 @@ function (param_type param, param_type param)
 * Code lines should be less than 80 columns and comments should be
   wrapped at 70 columns.
 
+* Variable declarations should be at the top of a block; C99 style
+  control variable declarations in for loops are also OK.
+
 Naming
 ------
 
 * Use lowercase_with_underscores for function, variable, and type
   names.
 
+* Except for variables with extremely small scope, and perhaps loop
+  indices, when naming variables and functions, err on the side of
+  verbosity.
+
 * All structs should be typedef'd to a name ending with _t.  If the
   struct has a tag, it should be the same as the typedef name, minus
   the trailing _t.
diff --git a/devel/nmbug/doc/.gitignore b/devel/nmbug/doc/.gitignore
new file mode 100644 (file)
index 0000000..4930881
--- /dev/null
@@ -0,0 +1,2 @@
+*.pyc
+_build
diff --git a/devel/nmbug/doc/Makefile b/devel/nmbug/doc/Makefile
new file mode 100644 (file)
index 0000000..7ea3ae7
--- /dev/null
@@ -0,0 +1,38 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+DOCBUILDDIR   := _build
+
+SRCDIR ?= .
+ALLSPHINXOPTS := -d $(DOCBUILDDIR)/doctrees $(SPHINXOPTS) $(SRCDIR)
+
+MAN_RST_FILES := $(shell find $(SRCDIR)/man* -name '*.rst')
+MAN_ROFF_FILES := $(patsubst $(SRCDIR)/man%.rst,$(DOCBUILDDIR)/man/man%,$(MAN_RST_FILES))
+MAN_GZIP_FILES := $(addsuffix .gz,$(MAN_ROFF_FILES))
+
+.PHONY: build-man
+build-man: $(MAN_GZIP_FILES)
+
+%.gz: %
+       rm -f $@ && gzip --stdout $^ > $@
+
+$(MAN_ROFF_FILES): $(DOCBUILDDIR)/.roff.stamp
+
+# By using $(DOCBUILDDIR)/.roff.stamp instead of $(MAN_ROFF_FILES), we
+# convey to make that a single invocation of this recipe builds all
+# of the roff files.  This prevents parallel make from starting an
+# instance of this recipe for each roff file.
+$(DOCBUILDDIR)/.roff.stamp $(MAN_ROFF_FILES): $(MAN_RST_FILES)
+       mkdir -p $(DOCBUILDDIR)
+       touch $(DOCBUILDDIR)/.roff.stamp
+       $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(DOCBUILDDIR)/man
+       for section in 1 5; do \
+           mkdir -p $(DOCBUILDDIR)/man/man$${section}; \
+           mv $(DOCBUILDDIR)/man/*.$${section} $(DOCBUILDDIR)/man/man$${section}; \
+       done
+
+clean:
+       rm -rf $(DOCBUILDDIR) $(SRCDIR)/conf.pyc
diff --git a/devel/nmbug/doc/conf.py b/devel/nmbug/doc/conf.py
new file mode 100644 (file)
index 0000000..29379d0
--- /dev/null
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+
+import os.path
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'notmuch'
+authors = 'Carl Worth and many others'
+copyright = '2009-2015, {0}'.format(authors)
+
+location = os.path.dirname(__file__)
+
+dirname = location
+while True:
+    version_file = os.path.join(dirname, 'version')
+    if os.path.exists(version_file):
+        with open(version_file,'r') as f:
+            version = f.read().strip()
+            break
+    if dirname == '/':
+        raise ValueError(
+            'no version file found in this directory or its ancestors')
+    dirname = os.path.dirname(dirname)
+
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+
+man_pages = [
+    ('man1/notmuch-report.1', 'notmuch-report',
+     'generate reports from notmuch queries', [authors], 1),
+    ('man5/notmuch-report.json.5', 'notmuch-report.json',
+     'configure notmuch-report', [authors], 5),
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+texinfo_no_detailmenu = True
+
+texinfo_documents = [
+    ('man1/notmuch-report.1', 'notmuch-report',
+     'generate reports from notmuch queries', authors, 'notmuch-report',
+     'generate reports from notmuch queries', 'Miscellaneous'),
+    ('man5/notmuch-report.json.5', 'notmuch-report.json',
+     'configure notmuch-report', authors, 'notmuch-report.json',
+     'configure notmuch-report', 'Miscellaneous'),
+]
diff --git a/devel/nmbug/doc/index.rst b/devel/nmbug/doc/index.rst
new file mode 100644 (file)
index 0000000..51ac59e
--- /dev/null
@@ -0,0 +1,17 @@
+Welcome to notmuch's dev-tool documentation!
+============================================
+
+Contents:
+
+.. toctree::
+   :titlesonly:
+
+   man1/notmuch-report.1
+   man5/notmuch-report.json.5
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/devel/nmbug/doc/man1/notmuch-report.1.rst b/devel/nmbug/doc/man1/notmuch-report.1.rst
new file mode 100644 (file)
index 0000000..dfd82df
--- /dev/null
@@ -0,0 +1,54 @@
+==============
+notmuch-report
+==============
+
+SYNOPSIS
+========
+
+**notmuch-report** [options ...]
+
+DESCRIPTION
+===========
+
+Generate HTML or plain-text reports showing query results.
+
+OPTIONS
+=======
+
+  ``-h``, ``--help``
+
+    Show a help message, including a list of available options, and
+    exit.
+
+  ``--text``
+    Output plain text instead of HTML.
+
+  ``--config`` <PATH>
+    Load config from given file.  The format is described in
+    **notmuch-report.json(5)**.  If this option is not set,
+    **notmuch-report** loads the config from the Git repository at
+    ``NMBGIT``.  See :ref:`NMBGIT <NMBGIT>` for details.
+
+  ``--list-views``
+    List available views (by title) and exit.
+
+  ``--get-query`` <VIEW>
+    Print the configured query for view matching the given title.
+
+ENVIRONMENT
+===========
+
+.. _NMBGIT:
+
+  ``NMBGIT``
+    If ``--config PATH`` is not set, **notmuch-report** will attempt
+    to load a config file named ``notmuch-report.json`` from the
+    ``config`` branch of the ``NMBGIT`` repository (defaulting to
+    ``~/.nmbug``).
+
+SEE ALSO
+========
+
+**notmuch(1)**, **notmuch-report.json(5)**, **notmuch-search(1)**,
+ **notmuch-tag(1)**
+
diff --git a/devel/nmbug/doc/man5/notmuch-report.json.5.rst b/devel/nmbug/doc/man5/notmuch-report.json.5.rst
new file mode 100644 (file)
index 0000000..4b5f84a
--- /dev/null
@@ -0,0 +1,129 @@
+==============
+notmuch-report
+==============
+
+NAME
+====
+
+notmuch-report.json - configure output for **notmuch-report(1)**
+
+DESCRIPTION
+===========
+
+The config file is JSON_ with the following fields:
+
+meta
+  An object with page-wide information
+
+  title
+    Page title used in the default header.
+
+  blurb
+    Introduction paragraph used in the default header.
+
+  header
+    `Python format string`_ for the HTML header.  Optional.  It is
+    formatted with the following context:
+
+    date
+      The current UTC date.
+
+    datetime
+      The current UTC date-time.
+
+    title
+      The **meta.title** value.
+
+    blurb
+      The **meta.blurb** value.
+
+    encoding
+      The encoding used for the output file.
+
+    inter_message_padding
+      0.25em, for consistent CSS generation.
+
+    border_radius
+      0.5em, for consistent CSS generation.
+
+  footer
+    `Python format string`_ for the HTML footer.  It is formatted with
+    the same context used for **meta.header**.  Optional.
+
+  message-url
+    `Python format string`_ for message-linking URLs.  Optional.
+    Defaults to linking Gmane_.  It is formatted with the following
+    context:
+
+    message-id
+      The quoted_ message ID.
+
+    subject
+      The message subject.
+
+views
+  An array of view objects, where each object has the following
+  fields:
+
+  title
+    Header text for the view.
+
+  comment
+    Paragraph describing the view in more detail.  Optional.
+
+  id
+    Anchor string for the view.  Optional, defaulting to a slugged
+    form of the view title
+
+  query
+    An array of strings, which will be joined with 'and' to form the
+    view query.
+
+.. _Gmane: http://gmane.org/
+.. _JSON: http://json.org/
+.. _Python format string: https://docs.python.org/3/library/string.html#formatstrings
+.. _quoted: https://docs.python.org/3/library/urllib.parse.html#urllib.parse.quote
+
+EXAMPLE
+=======
+
+::
+
+  {
+    "meta": {
+      "title": "Notmuch Patches",
+      "blurb": "For more information see <a href=\"http://notmuchmail.org/nmbug\">nmbug</a>",
+      "header": "<html><head></head><body><h1>{title}</h1><p>{blurb}</p><h2>Views</h2>",
+      "footer": "<hr><p>Generated: {datetime}</p></html>",
+      "message-url": "http://mid.gmane.org/{message-id}"
+    },
+    "views": [
+      {
+        "title": "Bugs",
+        "comment": "Unresolved bugs.",
+        "query": [
+          "tag:notmuch::bug",
+          "not tag:notmuch::fixed",
+          "not tag:notmuch::wontfix"
+        ]
+      },
+      {
+        "title": "Review",
+        "comment": "These patches are under review, or waiting for feedback.",
+        "id": "under-review",
+        "query": [
+          "tag:notmuch::patch",
+          "not tag:notmuch::pushed",
+          "not tag:notmuch::obsolete",
+          "not tag:notmuch::stale",
+          "not tag:notmuch::wontfix",
+          "(tag:notmuch::moreinfo or tag:notmuch::needs-review)"
+        ]
+      }
+    ]
+  }
+
+SEE ALSO
+========
+
+**notmuch(1)**, **notmuch-report(1)**, **notmuch-search(1)**, **notmuch-tag(1)**
index 81f582ce88bdea394833b16666c79eb813e184ed..0787b2ba9137f7ef01bfef7f0ac18b7fc46dc54a 100755 (executable)
@@ -608,6 +608,8 @@ def _index_tags():
                 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):])
diff --git a/devel/nmbug/nmbug-status b/devel/nmbug/nmbug-status
deleted file mode 100755 (executable)
index b36b6ad..0000000
+++ /dev/null
@@ -1,419 +0,0 @@
-#!/usr/bin/python
-#
-# Copyright (c) 2011-2012 David Bremner <david@tethera.net>
-#
-# dependencies
-#       - python 2.6 for json
-#       - argparse; either python 2.7, or install separately
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see http://www.gnu.org/licenses/ .
-
-"""Generate HTML for one or more notmuch searches.
-
-Messages matching each search are grouped by thread.  Each message
-that contains both a subject and message-id will have the displayed
-subject link to the Gmane view of the message.
-"""
-
-from __future__ import print_function
-from __future__ import unicode_literals
-
-import codecs
-import collections
-import datetime
-import email.utils
-try:  # Python 3
-    from urllib.parse import quote
-except ImportError:  # Python 2
-    from urllib import quote
-import json
-import argparse
-import os
-import re
-import sys
-import subprocess
-import xml.sax.saxutils
-
-
-_ENCODING = 'UTF-8'
-_PAGES = {}
-
-
-if not hasattr(collections, 'OrderedDict'):  # Python 2.6 or earlier
-    class _OrderedDict (dict):
-        "Just enough of a stub to get through Page._get_threads"
-        def __init__(self, *args, **kwargs):
-            super(_OrderedDict, self).__init__(*args, **kwargs)
-            self._keys = []  # record key order
-
-        def __setitem__(self, key, value):
-            super(_OrderedDict, self).__setitem__(key, value)
-            self._keys.append(key)
-
-        def values(self):
-            for key in self._keys:
-                yield self[key]
-
-
-    collections.OrderedDict = _OrderedDict
-
-
-class ConfigError (Exception):
-    """Errors with config file usage
-    """
-    pass
-
-
-def read_config(path=None, encoding=None):
-    "Read config from json file"
-    if not encoding:
-        encoding = _ENCODING
-    if path:
-        try:
-            with open(path, 'rb') as f:
-                config_bytes = f.read()
-        except IOError as e:
-            raise ConfigError('Could not read config from {}'.format(path))
-    else:
-        nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
-        branch = 'config'
-        filename = 'status-config.json'
-
-        # read only the first line from the pipe
-        sha1_bytes = subprocess.Popen(
-            ['git', '--git-dir', nmbhome, 'show-ref', '-s', '--heads', branch],
-            stdout=subprocess.PIPE).stdout.readline()
-        sha1 = sha1_bytes.decode(encoding).rstrip()
-        if not sha1:
-            raise ConfigError(
-                ("No local branch '{branch}' in {nmbgit}.  "
-                 'Checkout a local {branch} branch or explicitly set --config.'
-                ).format(branch=branch, nmbgit=nmbhome))
-
-        p = subprocess.Popen(
-            ['git', '--git-dir', nmbhome, 'cat-file', 'blob',
-             '{}:{}'.format(sha1, filename)],
-            stdout=subprocess.PIPE)
-        config_bytes, err = p.communicate()
-        status = p.wait()
-        if status != 0:
-            raise ConfigError(
-                ("Missing status-config.json in branch '{branch}' of"
-                 '{nmbgit}.  Add the file or explicitly set --config.'
-                ).format(branch=branch, nmbgit=nmbhome))
-
-    config_json = config_bytes.decode(encoding)
-    try:
-        return json.loads(config_json)
-    except ValueError as e:
-        if not path:
-            path = "{} in branch '{}' of {}".format(
-                filename, branch, nmbhome)
-        raise ConfigError(
-            'Could not parse JSON from the config file {}:\n{}'.format(
-                path, e))
-
-
-class Thread (list):
-    def __init__(self):
-        self.running_data = {}
-
-
-class Page (object):
-    def __init__(self, header=None, footer=None):
-        self.header = header
-        self.footer = footer
-
-    def write(self, database, views, stream=None):
-        if not stream:
-            try:  # Python 3
-                byte_stream = sys.stdout.buffer
-            except AttributeError:  # Python 2
-                byte_stream = sys.stdout
-            stream = codecs.getwriter(encoding=_ENCODING)(stream=byte_stream)
-        self._write_header(views=views, stream=stream)
-        for view in views:
-            self._write_view(database=database, view=view, stream=stream)
-        self._write_footer(views=views, stream=stream)
-
-    def _write_header(self, views, stream):
-        if self.header:
-            stream.write(self.header)
-
-    def _write_footer(self, views, stream):
-        if self.footer:
-            stream.write(self.footer)
-
-    def _write_view(self, database, view, stream):
-        # sort order, default to oldest-first
-        sort_key = view.get('sort', 'oldest-first')
-        # dynamically accept all values in Query.SORT
-        sort_attribute = sort_key.upper().replace('-', '_')
-        try:
-            sort = getattr(notmuch.Query.SORT, sort_attribute)
-        except AttributeError:
-            raise ConfigError('Invalid sort setting for {}: {!r}'.format(
-                view['title'], sort_key))
-        if 'query-string' not in view:
-            query = view['query']
-            view['query-string'] = ' and '.join(query)
-        q = notmuch.Query(database, view['query-string'])
-        q.set_sort(sort)
-        threads = self._get_threads(messages=q.search_messages())
-        self._write_view_header(view=view, stream=stream)
-        self._write_threads(threads=threads, stream=stream)
-
-    def _get_threads(self, messages):
-        threads = collections.OrderedDict()
-        for message in messages:
-            thread_id = message.get_thread_id()
-            if thread_id in threads:
-                thread = threads[thread_id]
-            else:
-                thread = Thread()
-                threads[thread_id] = thread
-            thread.running_data, display_data = self._message_display_data(
-                running_data=thread.running_data, message=message)
-            thread.append(display_data)
-        return list(threads.values())
-
-    def _write_view_header(self, view, stream):
-        pass
-
-    def _write_threads(self, threads, stream):
-        for thread in threads:
-            for message_display_data in thread:
-                stream.write(
-                    ('{date:10.10s} {from:20.20s} {subject:40.40s}\n'
-                     '{message-id-term:>72}\n'
-                     ).format(**message_display_data))
-            if thread != threads[-1]:
-                stream.write('\n')
-
-    def _message_display_data(self, running_data, message):
-        headers = ('thread-id', 'message-id', 'date', 'from', 'subject')
-        data = {}
-        for header in headers:
-            if header == 'thread-id':
-                value = message.get_thread_id()
-            elif header == 'message-id':
-                value = message.get_message_id()
-                data['message-id-term'] = 'id:"{0}"'.format(value)
-            elif header == 'date':
-                value = str(datetime.datetime.utcfromtimestamp(
-                    message.get_date()).date())
-            else:
-                value = message.get_header(header)
-            if header == 'from':
-                (value, addr) = email.utils.parseaddr(value)
-                if not value:
-                    value = addr.split('@')[0]
-            data[header] = value
-        next_running_data = data.copy()
-        for header, value in data.items():
-            if header in ['message-id', 'subject']:
-                continue
-            if value == running_data.get(header, None):
-                data[header] = ''
-        return (next_running_data, data)
-
-
-class HtmlPage (Page):
-    _slug_regexp = re.compile('\W+')
-
-    def _write_header(self, views, stream):
-        super(HtmlPage, self)._write_header(views=views, stream=stream)
-        stream.write('<ul>\n')
-        for view in views:
-            if 'id' not in view:
-                view['id'] = self._slug(view['title'])
-            stream.write(
-                '<li><a href="#{id}">{title}</a></li>\n'.format(**view))
-        stream.write('</ul>\n')
-
-    def _write_view_header(self, view, stream):
-        stream.write('<h3 id="{id}">{title}</h3>\n'.format(**view))
-        stream.write('<p>\n')
-        if 'comment' in view:
-            stream.write(view['comment'])
-            stream.write('\n')
-        for line in [
-                'The view is generated from the following query:',
-                '</p>',
-                '<p>',
-                '  <code>',
-                view['query-string'],
-                '  </code>',
-                '</p>',
-                ]:
-            stream.write(line)
-            stream.write('\n')
-
-    def _write_threads(self, threads, stream):
-        if not threads:
-            return
-        stream.write('<table>\n')
-        for thread in threads:
-            stream.write('  <tbody>\n')
-            for message_display_data in thread:
-                stream.write((
-                    '    <tr class="message-first">\n'
-                    '      <td>{date}</td>\n'
-                    '      <td><code>{message-id-term}</code></td>\n'
-                    '    </tr>\n'
-                    '    <tr class="message-last">\n'
-                    '      <td>{from}</td>\n'
-                    '      <td>{subject}</td>\n'
-                    '    </tr>\n'
-                    ).format(**message_display_data))
-            stream.write('  </tbody>\n')
-            if thread != threads[-1]:
-                stream.write(
-                    '  <tbody><tr><td colspan="2"><br /></td></tr></tbody>\n')
-        stream.write('</table>\n')
-
-    def _message_display_data(self, *args, **kwargs):
-        running_data, display_data = super(
-            HtmlPage, self)._message_display_data(
-                *args, **kwargs)
-        if 'subject' in display_data and 'message-id' in display_data:
-            d = {
-                'message-id': quote(display_data['message-id']),
-                'subject': xml.sax.saxutils.escape(display_data['subject']),
-                }
-            display_data['subject'] = (
-                '<a href="http://mid.gmane.org/{message-id}">{subject}</a>'
-                ).format(**d)
-        for key in ['message-id', 'from']:
-            if key in display_data:
-                display_data[key] = xml.sax.saxutils.escape(display_data[key])
-        return (running_data, display_data)
-
-    def _slug(self, string):
-        return self._slug_regexp.sub('-', string)
-
-parser = argparse.ArgumentParser(description=__doc__)
-parser.add_argument('--text', help='output plain text format',
-                    action='store_true')
-parser.add_argument('--config', help='load config from given file',
-                    metavar='PATH')
-parser.add_argument('--list-views', help='list views',
-                    action='store_true')
-parser.add_argument('--get-query', help='get query for view',
-                    metavar='VIEW')
-
-args = parser.parse_args()
-
-try:
-    config = read_config(path=args.config)
-except ConfigError as e:
-    print(e, file=sys.stderr)
-    sys.exit(1)
-
-header_template = config['meta'].get('header', '''<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
-  <title>{title}</title>
-  <style media="screen" type="text/css">
-    table {{
-      border-spacing: 0;
-    }}
-    tr.message-first td {{
-      padding-top: {inter_message_padding};
-    }}
-    tr.message-last td {{
-      padding-bottom: {inter_message_padding};
-    }}
-    td {{
-      padding-left: {border_radius};
-      padding-right: {border_radius};
-    }}
-    tr:first-child td:first-child {{
-      border-top-left-radius: {border_radius};
-    }}
-    tr:first-child td:last-child {{
-      border-top-right-radius: {border_radius};
-    }}
-    tr:last-child td:first-child {{
-      border-bottom-left-radius: {border_radius};
-    }}
-    tr:last-child td:last-child {{
-      border-bottom-right-radius: {border_radius};
-    }}
-    tbody:nth-child(4n+1) tr td {{
-      background-color: #ffd96e;
-    }}
-    tbody:nth-child(4n+3) tr td {{
-      background-color: #bce;
-    }}
-    hr {{
-      border: 0;
-      height: 1px;
-      color: #ccc;
-      background-color: #ccc;
-    }}
-  </style>
-</head>
-<body>
-<h2>{title}</h2>
-{blurb}
-</p>
-<h3>Views</h3>
-''')
-
-footer_template = config['meta'].get('footer', '''
-<hr>
-<p>Generated: {datetime}
-</body>
-</html>
-''')
-
-now = datetime.datetime.utcnow()
-context = {
-    'date': now,
-    'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
-    'title': config['meta']['title'],
-    'blurb': config['meta']['blurb'],
-    'encoding': _ENCODING,
-    'inter_message_padding': '0.25em',
-    'border_radius': '0.5em',
-    }
-
-_PAGES['text'] = Page()
-_PAGES['html'] = HtmlPage(
-    header=header_template.format(**context),
-    footer=footer_template.format(**context),
-    )
-
-if args.list_views:
-    for view in config['views']:
-        print(view['title'])
-    sys.exit(0)
-elif args.get_query != None:
-    for view in config['views']:
-        if args.get_query == view['title']:
-            print(' and '.join(view['query']))
-    sys.exit(0)
-else:
-    # only import notmuch if needed
-    import notmuch
-
-if args.text:
-    page = _PAGES['text']
-else:
-    page = _PAGES['html']
-
-db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
-page.write(database=db, views=config['views'])
diff --git a/devel/nmbug/notmuch-report b/devel/nmbug/notmuch-report
new file mode 100755 (executable)
index 0000000..87390c1
--- /dev/null
@@ -0,0 +1,440 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2011-2012 David Bremner <david@tethera.net>
+#
+# dependencies
+#       - python 2.6 for json
+#       - argparse; either python 2.7, or install separately
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see http://www.gnu.org/licenses/ .
+
+"""Generate text and/or HTML for one or more notmuch searches.
+
+Messages matching each search are grouped by thread.  Each message
+that contains both a subject and message-id will have the displayed
+subject link to an archive view of the message (defaulting to Gmane).
+"""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import codecs
+import collections
+import datetime
+import email.utils
+try:  # Python 3
+    from urllib.parse import quote
+except ImportError:  # Python 2
+    from urllib import quote
+import json
+import argparse
+import os
+import re
+import sys
+import subprocess
+import xml.sax.saxutils
+
+
+_ENCODING = 'UTF-8'
+_PAGES = {}
+
+
+if not hasattr(collections, 'OrderedDict'):  # Python 2.6 or earlier
+    class _OrderedDict (dict):
+        "Just enough of a stub to get through Page._get_threads"
+        def __init__(self, *args, **kwargs):
+            super(_OrderedDict, self).__init__(*args, **kwargs)
+            self._keys = []  # record key order
+
+        def __setitem__(self, key, value):
+            super(_OrderedDict, self).__setitem__(key, value)
+            self._keys.append(key)
+
+        def values(self):
+            for key in self._keys:
+                yield self[key]
+
+
+    collections.OrderedDict = _OrderedDict
+
+
+class ConfigError (Exception):
+    """Errors with config file usage
+    """
+    pass
+
+
+def read_config(path=None, encoding=None):
+    "Read config from json file"
+    if not encoding:
+        encoding = _ENCODING
+    if path:
+        try:
+            with open(path, 'rb') as f:
+                config_bytes = f.read()
+        except IOError as e:
+            raise ConfigError('Could not read config from {}'.format(path))
+    else:
+        nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
+        branch = 'config'
+        filename = 'notmuch-report.json'
+
+        # read only the first line from the pipe
+        sha1_bytes = subprocess.Popen(
+            ['git', '--git-dir', nmbhome, 'show-ref', '-s', '--heads', branch],
+            stdout=subprocess.PIPE).stdout.readline()
+        sha1 = sha1_bytes.decode(encoding).rstrip()
+        if not sha1:
+            raise ConfigError(
+                ("No local branch '{branch}' in {nmbgit}.  "
+                 'Checkout a local {branch} branch or explicitly set --config.'
+                ).format(branch=branch, nmbgit=nmbhome))
+
+        p = subprocess.Popen(
+            ['git', '--git-dir', nmbhome, 'cat-file', 'blob',
+             '{}:{}'.format(sha1, filename)],
+            stdout=subprocess.PIPE)
+        config_bytes, err = p.communicate()
+        status = p.wait()
+        if status != 0:
+            raise ConfigError(
+                ("Missing {filename} in branch '{branch}' of {nmbgit}.  "
+                 'Add the file or explicitly set --config.'
+                ).format(filename=filename, branch=branch, nmbgit=nmbhome))
+
+    config_json = config_bytes.decode(encoding)
+    try:
+        return json.loads(config_json)
+    except ValueError as e:
+        if not path:
+            path = "{} in branch '{}' of {}".format(
+                filename, branch, nmbhome)
+        raise ConfigError(
+            'Could not parse JSON from the config file {}:\n{}'.format(
+                path, e))
+
+
+class Thread (list):
+    def __init__(self):
+        self.running_data = {}
+
+
+class Page (object):
+    def __init__(self, header=None, footer=None):
+        self.header = header
+        self.footer = footer
+
+    def write(self, database, views, stream=None):
+        if not stream:
+            try:  # Python 3
+                byte_stream = sys.stdout.buffer
+            except AttributeError:  # Python 2
+                byte_stream = sys.stdout
+            stream = codecs.getwriter(encoding=_ENCODING)(stream=byte_stream)
+        self._write_header(views=views, stream=stream)
+        for view in views:
+            self._write_view(database=database, view=view, stream=stream)
+        self._write_footer(views=views, stream=stream)
+
+    def _write_header(self, views, stream):
+        if self.header:
+            stream.write(self.header)
+
+    def _write_footer(self, views, stream):
+        if self.footer:
+            stream.write(self.footer)
+
+    def _write_view(self, database, view, stream):
+        # sort order, default to oldest-first
+        sort_key = view.get('sort', 'oldest-first')
+        # dynamically accept all values in Query.SORT
+        sort_attribute = sort_key.upper().replace('-', '_')
+        try:
+            sort = getattr(notmuch.Query.SORT, sort_attribute)
+        except AttributeError:
+            raise ConfigError('Invalid sort setting for {}: {!r}'.format(
+                view['title'], sort_key))
+        if 'query-string' not in view:
+            query = view['query']
+            view['query-string'] = ' and '.join(
+                '( {} )'.format(q) for q in query)
+        q = notmuch.Query(database, view['query-string'])
+        q.set_sort(sort)
+        threads = self._get_threads(messages=q.search_messages())
+        self._write_view_header(view=view, stream=stream)
+        self._write_threads(threads=threads, stream=stream)
+
+    def _get_threads(self, messages):
+        threads = collections.OrderedDict()
+        for message in messages:
+            thread_id = message.get_thread_id()
+            if thread_id in threads:
+                thread = threads[thread_id]
+            else:
+                thread = Thread()
+                threads[thread_id] = thread
+            thread.running_data, display_data = self._message_display_data(
+                running_data=thread.running_data, message=message)
+            thread.append(display_data)
+        return list(threads.values())
+
+    def _write_view_header(self, view, stream):
+        pass
+
+    def _write_threads(self, threads, stream):
+        for thread in threads:
+            for message_display_data in thread:
+                stream.write(
+                    ('{date:10.10s} {from:20.20s} {subject:40.40s}\n'
+                     '{message-id-term:>72}\n'
+                     ).format(**message_display_data))
+            if thread != threads[-1]:
+                stream.write('\n')
+
+    def _message_display_data(self, running_data, message):
+        headers = ('thread-id', 'message-id', 'date', 'from', 'subject')
+        data = {}
+        for header in headers:
+            if header == 'thread-id':
+                value = message.get_thread_id()
+            elif header == 'message-id':
+                value = message.get_message_id()
+                data['message-id-term'] = 'id:"{0}"'.format(value)
+            elif header == 'date':
+                value = str(datetime.datetime.utcfromtimestamp(
+                    message.get_date()).date())
+            else:
+                value = message.get_header(header)
+            if header == 'from':
+                (value, addr) = email.utils.parseaddr(value)
+                if not value:
+                    value = addr.split('@')[0]
+            data[header] = value
+        next_running_data = data.copy()
+        for header, value in data.items():
+            if header in ['message-id', 'subject']:
+                continue
+            if value == running_data.get(header, None):
+                data[header] = ''
+        return (next_running_data, data)
+
+
+class HtmlPage (Page):
+    _slug_regexp = re.compile('\W+')
+
+    def __init__(self, message_url_template, **kwargs):
+        self.message_url_template = message_url_template
+        super(HtmlPage, self).__init__(**kwargs)
+
+    def _write_header(self, views, stream):
+        super(HtmlPage, self)._write_header(views=views, stream=stream)
+        stream.write('<ul>\n')
+        for view in views:
+            if 'id' not in view:
+                view['id'] = self._slug(view['title'])
+            stream.write(
+                '<li><a href="#{id}">{title}</a></li>\n'.format(**view))
+        stream.write('</ul>\n')
+
+    def _write_view_header(self, view, stream):
+        stream.write('<h3 id="{id}">{title}</h3>\n'.format(**view))
+        stream.write('<p>\n')
+        if 'comment' in view:
+            stream.write(view['comment'])
+            stream.write('\n')
+        for line in [
+                'The view is generated from the following query:',
+                '</p>',
+                '<p>',
+                '  <code>',
+                view['query-string'],
+                '  </code>',
+                '</p>',
+                ]:
+            stream.write(line)
+            stream.write('\n')
+
+    def _write_threads(self, threads, stream):
+        if not threads:
+            return
+        stream.write('<table>\n')
+        for thread in threads:
+            stream.write('  <tbody>\n')
+            for message_display_data in thread:
+                stream.write((
+                    '    <tr class="message-first">\n'
+                    '      <td>{date}</td>\n'
+                    '      <td><code>{message-id-term}</code></td>\n'
+                    '    </tr>\n'
+                    '    <tr class="message-last">\n'
+                    '      <td>{from}</td>\n'
+                    '      <td>{subject}</td>\n'
+                    '    </tr>\n'
+                    ).format(**message_display_data))
+            stream.write('  </tbody>\n')
+            if thread != threads[-1]:
+                stream.write(
+                    '  <tbody><tr><td colspan="2"><br /></td></tr></tbody>\n')
+        stream.write('</table>\n')
+
+    def _message_display_data(self, *args, **kwargs):
+        running_data, display_data = super(
+            HtmlPage, self)._message_display_data(
+                *args, **kwargs)
+        if 'subject' in display_data and 'message-id' in display_data:
+            d = {
+                'message-id': quote(display_data['message-id']),
+                'subject': xml.sax.saxutils.escape(display_data['subject']),
+                }
+            d['url'] = self.message_url_template.format(**d)
+            display_data['subject'] = (
+                '<a href="{url}">{subject}</a>'
+                ).format(**d)
+        for key in ['message-id', 'from']:
+            if key in display_data:
+                display_data[key] = xml.sax.saxutils.escape(display_data[key])
+        return (running_data, display_data)
+
+    def _slug(self, string):
+        return self._slug_regexp.sub('-', string)
+
+parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument(
+    '--text', action='store_true', help='output plain text format')
+parser.add_argument(
+    '--config', metavar='PATH',
+    help='load config from given file.  '
+        'The format is described in notmuch-report.json(5).')
+parser.add_argument(
+    '--list-views', action='store_true', help='list views')
+parser.add_argument(
+    '--get-query', metavar='VIEW', help='get query for view')
+
+
+args = parser.parse_args()
+
+try:
+    config = read_config(path=args.config)
+except ConfigError as e:
+    print(e, file=sys.stderr)
+    sys.exit(1)
+
+header_template = config['meta'].get('header', '''<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
+  <title>{title}</title>
+  <style media="screen" type="text/css">
+    h1 {{
+      font-size: 1.5em;
+    }}
+    h2 {{
+      font-size: 1.17em;
+    }}
+    h3 {{
+      font-size: 100%;
+    }}
+    table {{
+      border-spacing: 0;
+    }}
+    tr.message-first td {{
+      padding-top: {inter_message_padding};
+    }}
+    tr.message-last td {{
+      padding-bottom: {inter_message_padding};
+    }}
+    td {{
+      padding-left: {border_radius};
+      padding-right: {border_radius};
+    }}
+    tr:first-child td:first-child {{
+      border-top-left-radius: {border_radius};
+    }}
+    tr:first-child td:last-child {{
+      border-top-right-radius: {border_radius};
+    }}
+    tr:last-child td:first-child {{
+      border-bottom-left-radius: {border_radius};
+    }}
+    tr:last-child td:last-child {{
+      border-bottom-right-radius: {border_radius};
+    }}
+    tbody:nth-child(4n+1) tr td {{
+      background-color: #ffd96e;
+    }}
+    tbody:nth-child(4n+3) tr td {{
+      background-color: #bce;
+    }}
+    hr {{
+      border: 0;
+      height: 1px;
+      color: #ccc;
+      background-color: #ccc;
+    }}
+  </style>
+</head>
+<body>
+<h1>{title}</h1>
+<p>
+{blurb}
+</p>
+<h2>Views</h2>
+''')
+
+footer_template = config['meta'].get('footer', '''
+<hr>
+<p>Generated: {datetime}</p>
+</body>
+</html>
+''')
+
+now = datetime.datetime.utcnow()
+context = {
+    'date': now,
+    'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
+    'title': config['meta']['title'],
+    'blurb': config['meta']['blurb'],
+    'encoding': _ENCODING,
+    'inter_message_padding': '0.25em',
+    'border_radius': '0.5em',
+    }
+
+_PAGES['text'] = Page()
+_PAGES['html'] = HtmlPage(
+    header=header_template.format(**context),
+    footer=footer_template.format(**context),
+    message_url_template=config['meta'].get(
+        'message-url', 'http://mid.gmane.org/{message-id}'),
+    )
+
+if args.list_views:
+    for view in config['views']:
+        print(view['title'])
+    sys.exit(0)
+elif args.get_query != None:
+    for view in config['views']:
+        if args.get_query == view['title']:
+            print(' and '.join('( {} )'.format(q) for q in view['query']))
+    sys.exit(0)
+else:
+    # only import notmuch if needed
+    import notmuch
+
+if args.text:
+    page = _PAGES['text']
+else:
+    page = _PAGES['html']
+
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+page.write(database=db, views=config['views'])
diff --git a/devel/nmbug/notmuch-report.json b/devel/nmbug/notmuch-report.json
new file mode 100644 (file)
index 0000000..48b6f19
--- /dev/null
@@ -0,0 +1,70 @@
+{
+    "meta": {
+        "title": "Notmuch Patches",
+        "blurb": "For more information see <a href=\"http://notmuchmail.org/nmbug\">nmbug</a>"
+    },
+
+    "views": [
+       {
+           "comment": "Unresolved bugs (or just need tag updating).",
+           "query": [
+               "tag:notmuch::bug",
+               "not tag:notmuch::fixed",
+               "not tag:notmuch::wontfix"
+           ],
+           "title": "Bugs"
+       },
+       {
+           "comment": "These patches are under consideration for pushing.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::python",
+               "not tag:notmuch::vim",
+               "not tag:notmuch::wontfix",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (Core and Emacs)"
+       },
+       {
+           "comment": "These python related patches might be ready to push, or they might just need updated tags.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::wontfix",
+               " tag:notmuch::python",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (Python)"
+       },
+       {
+           "comment": "These vim related patches might be ready to push, or they might just need updated tags.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::wontfix",
+               "tag:notmuch::vim",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (vim)"
+       },
+       {
+           "comment": "These patches are under review, or waiting for feedback.",
+           "query": [
+               "tag:notmuch::patch",
+               "not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete",
+               "not tag:notmuch::stale",
+               "not tag:notmuch::wontfix",
+               "tag:notmuch::moreinfo or tag:notmuch::needs-review"
+           ],
+           "title": "Review"
+       }
+    ]
+}
diff --git a/devel/nmbug/status-config.json b/devel/nmbug/status-config.json
deleted file mode 100644 (file)
index b926946..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-{
-    "meta": {
-        "title": "Notmuch Patches",
-        "blurb": "For more information see <a href=\"http://notmuchmail.org/nmbug\">nmbug</a>"
-    },
-
-    "views": [
-       {
-           "comment": "Unresolved bugs (or just need tag updating).",
-           "query": [
-               "tag:notmuch::bug",
-               "not tag:notmuch::fixed",
-               "not tag:notmuch::wontfix"
-           ],
-           "title": "Bugs"
-       },
-       {
-           "comment": "These patches are under consideration for pushing.",
-           "query": [
-               "tag:notmuch::patch and not tag:notmuch::pushed",
-               "not tag:notmuch::obsolete and not tag:notmuch::wip",
-               "not tag:notmuch::stale and not tag:notmuch::contrib",
-               "not tag:notmuch::moreinfo",
-               "not tag:notmuch::python",
-               "not tag:notmuch::vim",
-               "not tag:notmuch::wontfix",
-               "not tag:notmuch::needs-review"
-           ],
-           "title": "Maybe Ready (Core and Emacs)"
-       },
-       {
-           "comment": "These python related patches might be ready to push, or they might just need updated tags.",
-           "query": [
-               "tag:notmuch::patch and not tag:notmuch::pushed",
-               "not tag:notmuch::obsolete and not tag:notmuch::wip",
-               "not tag:notmuch::stale and not tag:notmuch::contrib",
-               "not tag:notmuch::moreinfo",
-               "not tag:notmuch::wontfix",
-               " tag:notmuch::python",
-               "not tag:notmuch::needs-review"
-           ],
-           "title": "Maybe Ready (Python)"
-       },
-       {
-           "comment": "These vim related patches might be ready to push, or they might just need updated tags.",
-           "query": [
-               "tag:notmuch::patch and not tag:notmuch::pushed",
-               "not tag:notmuch::obsolete and not tag:notmuch::wip",
-               "not tag:notmuch::stale and not tag:notmuch::contrib",
-               "not tag:notmuch::moreinfo",
-               "not tag:notmuch::wontfix",
-               "tag:notmuch::vim",
-               "not tag:notmuch::needs-review"
-           ],
-           "title": "Maybe Ready (vim)"
-       },
-       {
-           "comment": "These patches are under review, or waiting for feedback.",
-           "query": [
-               "tag:notmuch::patch",
-               "not tag:notmuch::pushed",
-               "not tag:notmuch::obsolete",
-               "not tag:notmuch::stale",
-               "not tag:notmuch::wontfix",
-               "(tag:notmuch::moreinfo or tag:notmuch::needs-review)"
-           ],
-           "title": "Review"
-       }
-    ]
-}
index 8604a9f7d10bb3e62a620de25ae02d7031bf8c42..5a7578b863aad83adeccb0abd9ce619662f71056 100755 (executable)
@@ -175,6 +175,21 @@ case $news_date in
        append_emsg "Date '$news_date' in NEWS file is not in format (yyyy-mm-dd)"
 esac
 
+year=`exec date +%Y`
+echo -n "Checking that copyright in documentation contains 2009-$year... "
+# Read the value of variable `copyright' defined in 'doc/conf.py'.
+# As __file__ is not defined when python command is given from command line,
+# it is defined before contents of 'doc/conf.py' (which dereferences __file__)
+# is executed.
+copyrightline=`exec python -c "with open('doc/conf.py') as cf: __file__ = ''; exec(cf.read()); print(copyright)"`
+case $copyrightline in
+       *2009-$year*)
+               echo Yes. ;;
+       *)
+               echo No.
+               append_emsg "The copyright in doc/conf.py line '$copyrightline' does not contain '2009-$year'"
+esac
+
 if [ -n "$emsgs" ]
 then
        echo
diff --git a/devel/try-emacs-mua b/devel/try-emacs-mua
new file mode 100755 (executable)
index 0000000..b0a62c2
--- /dev/null
@@ -0,0 +1,157 @@
+#!/bin/sh
+:; set -x; exec "${EMACS:-emacs}" --debug-init --load "$0" "$@"; exit
+;;
+;; Try the notmuch emacs client located in ../emacs/ directory
+;;
+;; Run this without arguments; emacs window opens with some usage information
+;;
+;; Authors: Tomi Ollila <tomi.ollila@iki.fi>
+;;
+;; http://www.emacswiki.org/emacs/EmacsScripts was a useful starting point...
+;;
+;; Licence: GPLv3+
+;;
+
+(message "Starting '%s'" load-file-name)
+
+(set-buffer "*scratch*")
+
+(setq initial-buffer-choice nil
+      inhibit-startup-screen t)
+
+(when (featurep 'notmuch)
+  (insert "
+Notmuch has been loaded to this emacs (during processing of the init file)
+which means it is (most probably) loaded from different source than expected.
+
+Please run \"" (file-name-nondirectory load-file-name)
+"\" with '-q' (or '-Q') as an argument, to disable
+processing of the init file -- you can load it after emacs has started\n
+exit emacs (y or n)? ")
+  (if (y-or-n-p "exit emacs")
+      (kill-emacs)
+    (error "Stopped reading %s" load-file-name)))
+
+(let ((pdir (file-name-directory
+            (directory-file-name (file-name-directory load-file-name)))))
+  (unless (file-exists-p (concat pdir "emacs/notmuch-lib.el"))
+    (insert "Cannot find notmuch-emacs source directory
+while looking at: " pdir "emacs\n\nexit emacs (y or n)? ")
+    (if (y-or-n-p "exit emacs")
+       (kill-emacs)
+      (error "Stopped reading %s" load-file-name)))
+  (setq try-notmuch-source-directory (directory-file-name pdir)
+       try-notmuch-emacs-directory (concat pdir "emacs/")
+       load-path (cons try-notmuch-emacs-directory load-path)))
+
+;; they say advice doesn't work for primitives (functions from c source)
+;; well, these 'before' advice works for emacs 23.1 - 24.5 (at least)
+;; ...and for our purposes 24.3 is enough (there is no load-prefer-newer there)
+;; note also that the old, "obsolete" defadvice mechanism was used, but that
+;; is the only one available for emacs 23 and 24 up to 24.3.
+
+(if (boundp 'load-prefer-newer)
+    (defadvice require (before before-require activate)
+      (unless (featurep feature)
+       (message "require: %s" feature)))
+  ;; else: special require "short-circuit"; after load feature is provided...
+  ;; ... in notmuch sources we always use require and there are no loops
+  (defadvice require (before before-require activate)
+    (unless (featurep feature)
+      (message "require: %s" feature)
+      (let ((name (symbol-name feature)))
+       (if (and (string-match "^notmuch" name)
+                (file-newer-than-file-p
+                 (concat try-notmuch-emacs-directory name ".el")
+                 (concat try-notmuch-emacs-directory name ".elc")))
+           (load (concat try-notmuch-emacs-directory name ".el") nil nil t t)
+         )))))
+
+(insert "Found notmuch emacs client in " try-notmuch-emacs-directory "\n")
+
+(let ((notmuch-path (executable-find "notmuch")))
+  (insert "Notmuch CLI executable "
+         (if notmuch-path (concat "is " notmuch-path) "not found!") "\n"))
+
+(condition-case err
+;; "opportunistic" load-prefer-newer -- will be effective since emacs 24.4
+    (let ((load-prefer-newer t)
+         (force-load-messages t))
+      (require 'notmuch))
+  ;; specifying `debug' here lets the debugger run
+  ;; if `debug-on-error' is non-nil.
+  ((debug error)
+   (let ((error-message-string (error-message-string err)))
+     (insert "\nLoading notmuch failed: " error-message-string "\n")
+     (message "Loading notmuch failed: %s" error-message-string)
+     (insert "See *Messages* buffer for more information.\n")
+     (if init-file-user
+        (message "Hint: %s -q (or -Q) may help" load-file-name))
+     (pop-to-buffer "*Messages*")
+     (error "Stopped reading %s" load-file-name))))
+
+(insert "
+Go to the end of the following lines and type C-x C-e to evaluate
+(or C-j which is shorter but inserts evaluation results into buffer)
+
+To \"disable\" mail sending, evaluate
+* (setq message-send-mail-function (lambda () t))
+")
+
+(if (file-exists-p (concat try-notmuch-source-directory "/notmuch"))
+    (insert "
+To use accompanied notmuch binary from the same source, evaluate
+* (setq exec-path (cons \"" try-notmuch-source-directory  "\" exec-path))
+Note: Evaluating the above may be followed by unintended database
+upgrade and getting back to old version may require dump & restore.
+"))
+
+(if init-file-user ;; nil, if '-q' or '-Q' is given, but no '-u' 'USER'
+    (insert "
+Your init file was processed during emacs startup. If you want to test
+notmuch emacs mail client without your emacs init file interfering, Run\n\""
+(file-name-nondirectory load-file-name) "\" with '-q' (or '-Q') as an argument.
+")
+  (let ((emacs-init-file-name) (notmuch-init-file-name))
+    ;; determining init file name in startup.el/command-line is too complicated
+    ;; to be duplicated here; these 3 file names covers most of the users
+    (mapc (lambda (fn) (if (file-exists-p fn) (setq emacs-init-file-name fn)))
+         '("~/.emacs.d/init.el" "~/.emacs" "~/.emacs.el"))
+    (setq notmuch-init-file-name "~/.emacs.d/notmuch-config.el")
+    (unless (file-exists-p notmuch-init-file-name)
+       (setq notmuch-init-file-name nil))
+    (if (and emacs-init-file-name notmuch-init-file-name)
+       (insert "
+If you want to load your initialization files now, evaluate\n* (progn")
+      (if (or emacs-init-file-name notmuch-init-file-name)
+         (insert "
+If you want to load your initialization file now, evaluate\n*")))
+    (if emacs-init-file-name
+       (insert " (load \"" emacs-init-file-name "\")"))
+    (if notmuch-init-file-name
+       (insert " (load \"" notmuch-init-file-name "\")"))
+    (if (and emacs-init-file-name notmuch-init-file-name)
+       (insert ")"))
+    (if (or emacs-init-file-name notmuch-init-file-name)
+       (insert "\n")))
+  (if (>= emacs-major-version 24)
+      (insert "
+If you want to use packages (e.g. company from elpa) evaluate
+* (progn (require 'package) (package-initialize))
+")))
+
+(insert "
+To start notmuch (hello) screen, evaluate
+* (notmuch-hello)")
+
+(add-hook 'emacs-startup-hook
+         (lambda ()
+           (with-current-buffer "*scratch*"
+             (lisp-interaction-mode)
+             (goto-char (point-min))
+             (forward-line 2)
+             (set-buffer-modified-p nil))))
+
+;; Local Variables:
+;; mode: emacs-lisp
+;; End:
index 65adafeb1cef2e89b3409dcb9912c4e9bb993ac0..8b9329662bf01fade407f02f22ccb9e2c3c5f1b8 100644 (file)
@@ -12,7 +12,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'notmuch'
-copyright = u'2009-2015, Carl Worth and many others'
+copyright = u'2009-2016, Carl Worth and many others'
 
 location = os.path.dirname(__file__)
 
index cfbd4ea8dd190cc3d72370b7b01f151bd56a3ca9..d73f8f1c4ffd03da5889f1cbf9f17c48e5591c9b 100644 (file)
@@ -13,8 +13,10 @@ DESCRIPTION
 Constructs a reply template for a set of messages.
 
 To make replying to email easier, **notmuch reply** takes an existing
-set of messages and constructs a suitable mail template. The Reply-to:
-header (if any, otherwise From:) is used for the To: address. Unless
+set of messages and constructs a suitable mail template. Its To:
+address is set according to the original email in this way: if the
+Reply-to: header is present and different from any To:/Cc: address it
+is used, otherwise From: header is used. Unless
 ``--reply-to=sender`` is specified, values from the To: and Cc: headers
 are copied, but not including any of the current user's email addresses
 (as configured in primary\_mail or other\_email in the .notmuch-config
index 1109cfa6b09033769454ef8f8f27089197d72e7e..2d6aedbdfd5d5e1c98485ad9b27ee69e635431cd 100644 (file)
@@ -20,6 +20,7 @@ emacs_sources := \
        $(dir)/notmuch-print.el \
        $(dir)/notmuch-version.el \
        $(dir)/notmuch-jump.el \
+       $(dir)/notmuch-company.el
 
 $(dir)/notmuch-version.el: $(dir)/Makefile.local version.stamp
 $(dir)/notmuch-version.el: $(srcdir)/$(dir)/notmuch-version.el.tmpl
@@ -52,6 +53,10 @@ $(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources)
 $(dir)/.eldeps.x: $(dir)/.eldeps
        @cmp -s $^ $@ || cp $^ $@
 -include $(dir)/.eldeps.x
+
+# Add the one dependency make-deps.el does not have visibility to.
+$(dir)/notmuch-lib.elc: $(dir)/notmuch-version.elc
+
 endif
 CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp $(dir)/.eldeps.x
 
index 60af60afffa4e7faeaadae4a95bbcbe0adc43d01..77550602e5e1c08132b43a456b4d229fc40b2a1b 100644 (file)
@@ -143,3 +143,5 @@ If no break point is found, return nil."
          t)))
 
 (provide 'coolj)
+
+;;; coolj.el ends here
index a1cd731fb08e46aa924a4559bcbf5afa35d16377..24c1a45745821cc1840f9cd404e6542d66e3dedb 100644 (file)
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: Austin Clements <aclements@csail.mit.edu>
 
+;;; Code:
+
 (defun batch-make-deps ()
   "Invoke `make-deps' for each file on the command line."
 
@@ -64,3 +66,5 @@ rules will be given relative to DIR, or `default-directory'."
                                 (file-name-sans-extension
                                  (file-relative-name fname dir)))))))))
       (end-of-file nil))))
+
+;;; make-deps.el ends here
index fde3c1b2b861fb15063abe5fb4d480084149c88c..aafbe5fb8328a4ce1b92a9aa482ffa36dbd7fd6e 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-address.el --- address completion with notmuch
+;;; notmuch-address.el --- address completion with notmuch
 ;;
 ;; Copyright Â© David Edmondson
 ;;
 ;;
 ;; Authors: David Edmondson <dme@dme.org>
 
-(require 'message)
+;;; Code:
 
+(require 'message)
+(require 'notmuch-parser)
+(require 'notmuch-lib)
+(require 'notmuch-company)
 ;;
+(declare-function company-manual-begin "company")
 
-(defcustom notmuch-address-command "notmuch-addresses"
+(defcustom notmuch-address-command 'internal
   "The command which generates possible addresses. It must take a
 single argument and output a list of possible matches, one per
-line."
-  :type 'string
+line. The default value of `internal' uses built-in address
+completion."
+  :type '(radio
+         (const :tag "Use internal address completion" internal)
+         (const :tag "Disable address completion" nil)
+         (string :tag "Use external completion command" "notmuch-addresses"))
   :group 'notmuch-send
   :group 'notmuch-external)
 
@@ -42,53 +51,105 @@ to know how address selection is made by default."
   :group 'notmuch-send
   :group 'notmuch-external)
 
+(defvar notmuch-address-last-harvest 0
+  "Time of last address harvest")
+
+(defvar notmuch-address-completions (make-hash-table :test 'equal)
+  "Hash of email addresses for completion during email composition.
+  This variable is set by calling `notmuch-address-harvest'.")
+
+(defvar notmuch-address-full-harvest-finished nil
+  "t indicates that full completion address harvesting has been
+finished")
+
 (defun notmuch-address-selection-function (prompt collection initial-input)
   "Call (`completing-read'
       PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)"
   (completing-read
    prompt collection nil nil initial-input 'notmuch-address-history))
 
-(defvar notmuch-address-message-alist-member
-  '("^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):"
-             . notmuch-address-expand-name))
+(defvar notmuch-address-completion-headers-regexp
+  "^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):")
 
 (defvar notmuch-address-history nil)
 
 (defun notmuch-address-message-insinuate ()
-  (unless (memq notmuch-address-message-alist-member message-completion-alist)
-    (setq message-completion-alist
-         (push notmuch-address-message-alist-member message-completion-alist))))
+  (message "calling notmuch-address-message-insinuate is no longer needed"))
+
+(defcustom notmuch-address-use-company t
+  "If available, use company mode for address completion"
+  :type 'boolean
+  :group 'notmuch-send)
+
+(defun notmuch-address-setup ()
+  (let* ((use-company (and notmuch-address-use-company
+                          (eq notmuch-address-command 'internal)
+                          (require 'company nil t)))
+        (pair (cons notmuch-address-completion-headers-regexp
+                    (if use-company
+                        #'company-manual-begin
+                      #'notmuch-address-expand-name))))
+      (when use-company
+       (notmuch-company-setup))
+      (unless (memq pair message-completion-alist)
+       (setq message-completion-alist
+             (push pair message-completion-alist)))))
+
+(defun notmuch-address-matching (substring)
+  "Returns a list of completion candidates matching SUBSTRING.
+The candidates are taken from `notmuch-address-completions'."
+  (let ((candidates)
+       (re (regexp-quote substring)))
+    (maphash (lambda (key val)
+              (when (string-match re key)
+                (push key candidates)))
+            notmuch-address-completions)
+    candidates))
 
 (defun notmuch-address-options (original)
-  (process-lines notmuch-address-command original))
+  "Returns a list of completion candidates. Uses either
+elisp-based implementation or older implementation requiring
+external commands."
+  (cond
+   ((eq notmuch-address-command 'internal)
+    (when (not notmuch-address-full-harvest-finished)
+      ;; First, run quick synchronous harvest based on what the user
+      ;; entered so far
+      (notmuch-address-harvest (format "to:%s*" original) t))
+    (prog1 (notmuch-address-matching original)
+      ;; Then start the (potentially long-running) full asynchronous harvest if necessary
+      (notmuch-address-harvest-trigger)))
+   (t
+    (process-lines notmuch-address-command original))))
 
 (defun notmuch-address-expand-name ()
-  (let* ((end (point))
-        (beg (save-excursion
-               (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
-               (goto-char (match-end 0))
-               (point)))
-        (orig (buffer-substring-no-properties beg end))
-        (completion-ignore-case t)
-        (options (with-temp-message "Looking for completion candidates..."
-                   (notmuch-address-options orig)))
-        (num-options (length options))
-        (chosen (cond
-                 ((eq num-options 0)
-                  nil)
-                 ((eq num-options 1)
-                  (car options))
-                 (t
-                  (funcall notmuch-address-selection-function
-                           (format "Address (%s matches): " num-options)
-                           (cdr options) (car options))))))
-    (if chosen
-       (progn
-         (push chosen notmuch-address-history)
-         (delete-region beg end)
-         (insert chosen))
-      (message "No matches.")
-      (ding))))
+  (when notmuch-address-command
+    (let* ((end (point))
+          (beg (save-excursion
+                 (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+                 (goto-char (match-end 0))
+                 (point)))
+          (orig (buffer-substring-no-properties beg end))
+          (completion-ignore-case t)
+          (options (with-temp-message "Looking for completion candidates..."
+                     (notmuch-address-options orig)))
+          (num-options (length options))
+          (chosen (cond
+                   ((eq num-options 0)
+                    nil)
+                   ((eq num-options 1)
+                    (car options))
+                   (t
+                    (funcall notmuch-address-selection-function
+                             (format "Address (%s matches): " num-options)
+                             (cdr options) (car options))))))
+      (if chosen
+         (progn
+           (push chosen notmuch-address-history)
+           (delete-region beg end)
+           (insert chosen))
+       (message "No matches.")
+       (ding)))))
 
 ;; Copied from `w3m-which-command'.
 (defun notmuch-address-locate-command (command)
@@ -109,11 +170,85 @@ to know how address selection is made by default."
                           (not (file-directory-p bin))))
              (throw 'found-command bin))))))))
 
-;; If we can find the program specified by `notmuch-address-command',
-;; insinuate ourselves into `message-mode'.
-(when (notmuch-address-locate-command notmuch-address-command)
-  (notmuch-address-message-insinuate))
+(defun notmuch-address-harvest-addr (result)
+  (let ((name-addr (plist-get result :name-addr)))
+    (puthash name-addr t notmuch-address-completions)))
+
+(defun notmuch-address-harvest-handle-result (obj)
+  (notmuch-address-harvest-addr obj))
+
+(defun notmuch-address-harvest-filter (proc string)
+  (when (buffer-live-p (process-buffer proc))
+    (with-current-buffer (process-buffer proc)
+      (save-excursion
+       (goto-char (point-max))
+       (insert string))
+      (notmuch-sexp-parse-partial-list
+       'notmuch-address-harvest-handle-result (process-buffer proc)))))
+
+(defvar notmuch-address-harvest-procs '(nil . nil)
+  "The currently running harvests.
+
+The car is a partial harvest, and the cdr is a full harvest")
+
+(defun notmuch-address-harvest (&optional filter-query synchronous callback)
+  "Collect addresses completion candidates. It queries the
+notmuch database for all messages sent by the user optionally
+matching FILTER-QUERY (if not nil). It collects the destination
+addresses from those messages and stores them in
+`notmuch-address-completions'. Address harvesting may take some
+time so the address collection runs asynchronously unless
+SYNCHRONOUS is t. In case of asynchronous execution, CALLBACK is
+called when harvesting finishes."
+  (let* ((from-me-query (mapconcat (lambda (x) (concat "from:" x)) (notmuch-user-emails) " or "))
+        (query (if filter-query
+                   (format "(%s) and (%s)" from-me-query filter-query)
+                 from-me-query))
+        (args `("address" "--format=sexp" "--format-version=2"
+                "--output=recipients"
+                "--deduplicate=address"
+                ,query)))
+    (if synchronous
+       (mapc #'notmuch-address-harvest-addr
+                                  (apply 'notmuch-call-notmuch-sexp args))
+      ;; Asynchronous
+      (let* ((current-proc (if filter-query
+                              (car notmuch-address-harvest-procs)
+                            (cdr notmuch-address-harvest-procs)))
+            (proc-name (format "notmuch-address-%s-harvest"
+                               (if filter-query "partial" "full")))
+            (proc-buf (concat " *" proc-name "*")))
+       ;; Kill any existing process
+       (when current-proc
+         (kill-buffer (process-buffer current-proc))) ; this also kills the process
+
+       (setq current-proc
+             (apply 'notmuch-start-notmuch proc-name proc-buf
+                    callback                           ; process sentinel
+                    args))
+       (set-process-filter current-proc 'notmuch-address-harvest-filter)
+       (set-process-query-on-exit-flag current-proc nil)
+       (if filter-query
+           (setcar notmuch-address-harvest-procs current-proc)
+         (setcdr notmuch-address-harvest-procs current-proc)))))
+  ;; return value
+  nil)
+
+(defun notmuch-address-harvest-trigger ()
+  (let ((now (float-time)))
+    (when (> (- now notmuch-address-last-harvest) 86400)
+      (setq notmuch-address-last-harvest now)
+      (notmuch-address-harvest nil nil
+                              (lambda (proc event)
+                                ;; If harvest fails, we want to try
+                                ;; again when the trigger is next
+                                ;; called
+                                (if (string= event "finished\n")
+                                    (setq notmuch-address-full-harvest-finished t)
+                                  (setq notmuch-address-last-harvest 0)))))))
 
 ;;
 
 (provide 'notmuch-address)
+
+;;; notmuch-address.el ends here
diff --git a/emacs/notmuch-company.el b/emacs/notmuch-company.el
new file mode 100644 (file)
index 0000000..b881d6d
--- /dev/null
@@ -0,0 +1,88 @@
+;;; notmuch-company.el --- Mail address completion for notmuch via company-mode  -*- lexical-binding: t -*-
+
+;; Authors: Trevor Jim <tjim@mac.com>
+;;         Michal Sojka <sojkam1@fel.cvut.cz>
+;;
+;; Keywords: mail, completion
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; To enable this, install company mode (https://company-mode.github.io/)
+;;
+;; NB company-minimum-prefix-length defaults to 3 so you don't get
+;; completion unless you type 3 characters
+
+;;; Code:
+
+(eval-when-compile (require 'cl))
+
+(defvar notmuch-company-last-prefix nil)
+(make-variable-buffer-local 'notmuch-company-last-prefix)
+(declare-function company-begin-backend "company")
+(declare-function company-grab "company")
+(declare-function company-mode "company")
+(declare-function company-manual-begin "company")
+(defvar company-backends)
+
+(declare-function notmuch-address-harvest "notmuch-address")
+(declare-function notmuch-address-harvest-trigger "notmuch-address")
+(declare-function notmuch-address-matching "notmuch-address")
+(defvar notmuch-address-full-harvest-finished)
+(defvar notmuch-address-completion-headers-regexp)
+
+;;;###autoload
+(defun notmuch-company-setup ()
+  (company-mode)
+  (make-local-variable 'company-backends)
+  (setq company-backends '(notmuch-company)))
+
+;;;###autoload
+(defun notmuch-company (command &optional arg &rest _ignore)
+  "`company-mode' completion back-end for `notmuch'."
+  (interactive (list 'interactive))
+  (require 'company)
+  (let ((case-fold-search t)
+       (completion-ignore-case t))
+    (case command
+      (interactive (company-begin-backend 'notmuch-company))
+      (prefix (and (derived-mode-p 'message-mode)
+                  (looking-back (concat notmuch-address-completion-headers-regexp ".*")
+                                (line-beginning-position))
+                  (setq notmuch-company-last-prefix (company-grab "[:,][ \t]*\\(.*\\)" 1 (point-at-bol)))))
+      (candidates (cond
+                  (notmuch-address-full-harvest-finished
+                   ;; Update harvested addressed from time to time
+                   (notmuch-address-harvest-trigger)
+                   (notmuch-address-matching arg))
+                  (t
+                   (cons :async
+                         (lambda (callback)
+                           ;; First run quick asynchronous harvest based on what the user entered so far
+                           (notmuch-address-harvest
+                            (format "to:%s*" arg) nil
+                            (lambda (_proc _event)
+                              (funcall callback (notmuch-address-matching arg))
+                              ;; Then start the (potentially long-running) full asynchronous harvest if necessary
+                              (notmuch-address-harvest-trigger))))))))
+      (match (if (string-match notmuch-company-last-prefix arg)
+                (match-end 0)
+              0))
+      (no-cache t))))
+
+
+(provide 'notmuch-company)
+
+;;; notmuch-company.el ends here
index 52338249a82867a4e0fea4fb7788264fc9b15bd4..004463c316ea143bb6a2082ffb97a068ebc245db 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-crypto.el --- functions for handling display of cryptographic metadata.
+;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata.
 ;;
 ;; Copyright Â© Jameson Rollins
 ;;
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: Jameson Rollins <jrollins@finestructure.net>
 
+;;; Code:
+
 (require 'notmuch-lib)
 
 (defcustom notmuch-crypto-process-mime nil
@@ -110,8 +112,8 @@ mode."
        (setq label (concat "Bad signature (claimed key ID " keyid ")"))
        (setq face 'notmuch-crypto-signature-bad)))
      (t
-      (setq label "Unknown signature status")
-      (if status (setq label (concat label " \"" status "\"")))))
+      (setq label (concat "Unknown signature status"
+                         (if status (concat ": " status))))))
     (insert-button
      (concat "[ " label " ]")
      :type 'notmuch-crypto-status-button-type
@@ -161,7 +163,8 @@ mode."
      ((string= status "bad")
       (setq label "Decryption error"))
      (t
-      (setq label (concat "Unknown encstatus \"" status "\""))))
+      (setq label (concat "Unknown encryption status"
+                         (if status (concat ": " status))))))
     (insert-button
      (concat "[ " label " ]")
      :type 'notmuch-crypto-status-button-type
@@ -173,3 +176,5 @@ mode."
 ;;
 
 (provide 'notmuch-crypto)
+
+;;; notmuch-crypto.el ends here
index 8bde808fae6cc09fffb7631be2bf8f3fb67aec4c..9495c1a4874b589d003f9132d9e3c60c062933f7 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-hello.el --- welcome to notmuch, a frontend
+;;; notmuch-hello.el --- welcome to notmuch, a frontend
 ;;
 ;; Copyright Â© David Edmondson
 ;;
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: David Edmondson <dme@dme.org>
 
+;;; Code:
+
 (eval-when-compile (require 'cl))
 (require 'widget)
 (require 'wid-edit) ; For `widget-forward'.
@@ -652,8 +654,12 @@ with `notmuch-hello-query-counts'."
 
 (defvar notmuch-hello-mode-map
   (let ((map (if (fboundp 'make-composed-keymap)
-                ;; Inherit both widget-keymap and notmuch-common-keymap
-                (make-composed-keymap widget-keymap)
+                ;; Inherit both widget-keymap and
+                ;; notmuch-common-keymap. We have to use
+                ;; make-sparse-keymap to force this to be a new
+                ;; keymap (so that when we modify map it does not
+                ;; modify widget-keymap).
+                (make-composed-keymap (list (make-sparse-keymap) widget-keymap))
               ;; Before Emacs 24, keymaps didn't support multiple
               ;; inheritance,, so just copy the widget keymap since
               ;; it's unlikely to change.
@@ -668,6 +674,31 @@ with `notmuch-hello-query-counts'."
 (defun notmuch-hello-mode ()
  "Major mode for convenient notmuch navigation. This is your entry portal into notmuch.
 
+Saved searches are \"bookmarks\" for arbitrary queries. Hit RET
+or click on a saved search to view matching threads. Edit saved
+searches with the `edit' button. Type `\\[notmuch-jump-search]'
+in any Notmuch screen for quick access to saved searches that
+have shortcut keys.
+
+Type new searches in the search box and hit RET to view matching
+threads. Hit RET in a recent search box to re-submit a previous
+search. Edit it first if you like. Save a recent search to saved
+searches with the `save' button.
+
+Hit `\\[notmuch-search]' or `\\[notmuch-tree]' in any Notmuch
+screen to search for messages and view matching threads or
+messages, respectively. Recent searches are available in the
+minibuffer history.
+
+Expand the all tags view with the `show' button (and collapse
+again with the `hide' button). Hit RET or click on a tag name to
+view matching threads.
+
+Hit `\\[notmuch-refresh-this-buffer]' to refresh the screen and
+`\\[notmuch-bury-or-kill-this-buffer]' to quit.
+
+The screen may be customized via `\\[customize]'.
+
 Complete list of currently available key bindings:
 
 \\{notmuch-hello-mode-map}"
@@ -903,20 +934,19 @@ following:
 (defun notmuch-hello-insert-footer ()
   "Insert the notmuch-hello footer."
   (let ((start (point)))
-    (widget-insert "Type a search query and hit RET to view matching threads.\n")
-    (when notmuch-search-history
-      (widget-insert "Hit RET to re-submit a previous search. Edit it first if you like.\n")
-      (widget-insert "Save recent searches with the `save' button.\n"))
-    (when notmuch-saved-searches
-      (widget-insert "Edit saved searches with the `edit' button.\n"))
-    (widget-insert "Hit RET or click on a saved search or tag name to view matching threads.\n")
-    (widget-insert "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
+    (widget-insert "Hit `?' for context-sensitive help in any Notmuch screen.\n")
+    (widget-insert "Customize ")
+    (widget-create 'link
+                  :notify (lambda (&rest ignore)
+                            (customize-group 'notmuch))
+                  :button-prefix "" :button-suffix ""
+                  "Notmuch")
+    (widget-insert " or ")
     (widget-create 'link
                   :notify (lambda (&rest ignore)
                             (customize-variable 'notmuch-hello-sections))
                   :button-prefix "" :button-suffix ""
-                  "Customize")
-    (widget-insert " this page.")
+                  "this page.")
     (let ((fill-column (- (window-width) notmuch-hello-indent)))
       (center-region start (point)))))
 
@@ -988,3 +1018,5 @@ following:
 ;;
 
 (provide 'notmuch-hello)
+
+;;; notmuch-hello.el ends here
index 506ae2c8e399785c9f56169a1f16589c5d3ad807..fd770f1ec50e39f81afe481374fafbbc2f97b72a 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-jump.el --- User-friendly shortcut keys
+;;; notmuch-jump.el --- User-friendly shortcut keys
 ;;
 ;; Copyright Â© Austin Clements
 ;;
@@ -20,6 +20,8 @@
 ;; Authors: Austin Clements <aclements@csail.mit.edu>
 ;;          David Edmondson <dme@dme.org>
 
+;;; Code:
+
 (eval-when-compile (require 'cl))
 
 (require 'notmuch-lib)
@@ -176,3 +178,5 @@ buffer."
 ;;
 
 (provide 'notmuch-jump)
+
+;;; notmuch-jump.el ends here
index 201d7ec8593f376c1ae9f20a3671eff52535e4bc..78978ee34c9b806438c3fd9c497a11c11f9eb41c 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-lib.el --- common variables, functions and function declarations
+;;; notmuch-lib.el --- common variables, functions and function declarations
 ;;
 ;; Copyright Â© Carl Worth
 ;;
@@ -21,6 +21,8 @@
 
 ;; This is an part of an emacs-based interface to the notmuch mail system.
 
+;;; Code:
+
 (require 'mm-view)
 (require 'mm-decode)
 (require 'cl)
@@ -232,6 +234,9 @@ on the command line, and then retry your notmuch command")))
   "Return the user.other_email value (as a list) from the notmuch configuration."
   (split-string (notmuch-config-get "user.other_email") "\n" t))
 
+(defun notmuch-user-emails ()
+  (cons (notmuch-user-primary-email) (notmuch-user-other-email)))
+
 (defun notmuch-poll ()
   "Run \"notmuch new\" or an external script to import mail.
 
@@ -240,8 +245,9 @@ depending on the value of `notmuch-poll-script'."
   (interactive)
   (if (stringp notmuch-poll-script)
       (unless (string= notmuch-poll-script "")
-       (call-process notmuch-poll-script nil nil))
-    (call-process notmuch-command nil nil nil "new")))
+       (unless (equal (call-process notmuch-poll-script nil nil) 0)
+         (error "Notmuch: poll script `%s' failed!" notmuch-poll-script)))
+    (notmuch-call-notmuch-process "new")))
 
 (defun notmuch-bury-or-kill-this-buffer ()
   "Undisplay the current buffer.
@@ -516,11 +522,23 @@ This replaces spaces, percents, and double quotes in STR with
     "multipart/related"
     ))
 
-(defun notmuch-multipart/alternative-choose (types)
-  "Return a list of preferred types from the given list of types"
+(defun notmuch-multipart/alternative-determine-discouraged (msg)
+  "Return the discouraged alternatives for the specified message."
+  ;; If a function, return the result of calling it.
+  (if (functionp notmuch-multipart/alternative-discouraged)
+      (funcall notmuch-multipart/alternative-discouraged msg)
+    ;; Otherwise simply return the value of the variable, which is
+    ;; assumed to be a list of discouraged alternatives. This is the
+    ;; default behaviour.
+    notmuch-multipart/alternative-discouraged))
+
+(defun notmuch-multipart/alternative-choose (msg types)
+  "Return a list of preferred types from the given list of types
+for this message, if present."
   ;; Based on `mm-preferred-alternative-precedence'.
-  (let ((seq types))
-    (dolist (pref (reverse notmuch-multipart/alternative-discouraged))
+  (let ((discouraged (notmuch-multipart/alternative-determine-discouraged msg))
+       (seq types))
+    (dolist (pref (reverse discouraged))
       (dolist (elem (copy-sequence seq))
        (when (string-match pref elem)
          (setq seq (nconc (delete elem seq) (list elem))))))
@@ -533,6 +551,34 @@ the given type."
    (lambda (part) (notmuch-match-content-type (plist-get part :content-type) type))
    parts))
 
+(defun notmuch--get-bodypart-raw (msg part process-crypto binaryp cache)
+  (let* ((plist-elem (if binaryp :content-binary :content))
+        (data (or (plist-get part plist-elem)
+                  (with-temp-buffer
+                    ;; Emacs internally uses a UTF-8-like multibyte string
+                    ;; representation by default (regardless of the coding
+                    ;; system, which only affects how it goes from outside data
+                    ;; to this internal representation).  This *almost* never
+                    ;; matters.  Annoyingly, it does matter if we use this data
+                    ;; in an image descriptor, since Emacs will use its internal
+                    ;; data buffer directly and this multibyte representation
+                    ;; corrupts binary image formats.  Since the caller is
+                    ;; asking for binary data, a unibyte string is a more
+                    ;; appropriate representation anyway.
+                    (when binaryp
+                      (set-buffer-multibyte nil))
+                    (let ((args `("show" "--format=raw"
+                                  ,(format "--part=%s" (plist-get part :id))
+                                  ,@(when process-crypto '("--decrypt"))
+                                  ,(notmuch-id-to-query (plist-get msg :id))))
+                          (coding-system-for-read
+                           (if binaryp 'no-conversion 'utf-8)))
+                      (apply #'call-process notmuch-command nil '(t nil) nil args)
+                      (buffer-string))))))
+    (when (and cache data)
+      (plist-put part plist-elem data))
+    data))
+
 (defun notmuch-get-bodypart-binary (msg part process-crypto &optional cache)
   "Return the unprocessed content of PART in MSG as a unibyte string.
 
@@ -543,57 +589,18 @@ this does no charset conversion.
 
 If CACHE is non-nil, the content of this part will be saved in
 MSG (if it isn't already)."
-  (let ((data (plist-get part :binary-content)))
-    (when (not data)
-      (let ((args `("show" "--format=raw"
-                   ,(format "--part=%d" (plist-get part :id))
-                   ,@(when process-crypto '("--decrypt"))
-                   ,(notmuch-id-to-query (plist-get msg :id)))))
-       (with-temp-buffer
-         ;; Emacs internally uses a UTF-8-like multibyte string
-         ;; representation by default (regardless of the coding
-         ;; system, which only affects how it goes from outside data
-         ;; to this internal representation).  This *almost* never
-         ;; matters.  Annoyingly, it does matter if we use this data
-         ;; in an image descriptor, since Emacs will use its internal
-         ;; data buffer directly and this multibyte representation
-         ;; corrupts binary image formats.  Since the caller is
-         ;; asking for binary data, a unibyte string is a more
-         ;; appropriate representation anyway.
-         (set-buffer-multibyte nil)
-         (let ((coding-system-for-read 'no-conversion))
-           (apply #'call-process notmuch-command nil '(t nil) nil args)
-           (setq data (buffer-string)))))
-      (when cache
-       ;; Cheat.  part is non-nil, and `plist-put' always modifies
-       ;; the list in place if it's non-nil.
-       (plist-put part :binary-content data)))
-    data))
+  (notmuch--get-bodypart-raw msg part process-crypto t cache))
 
 (defun notmuch-get-bodypart-text (msg part process-crypto &optional cache)
   "Return the text content of PART in MSG.
 
 This returns the content of the given part as a multibyte Lisp
 string after performing content transfer decoding and any
-necessary charset decoding.  It is an error to use this for
-non-text/* parts.
+necessary charset decoding.
 
 If CACHE is non-nil, the content of this part will be saved in
 MSG (if it isn't already)."
-  (let ((content (plist-get part :content)))
-    (when (not content)
-      ;; Use show --format=sexp to fetch decoded content
-      (let* ((args `("show" "--format=sexp" "--include-html"
-                    ,(format "--part=%s" (plist-get part :id))
-                    ,@(when process-crypto '("--decrypt"))
-                    ,(notmuch-id-to-query (plist-get msg :id))))
-            (npart (apply #'notmuch-call-notmuch-sexp args)))
-       (setq content (plist-get npart :content))
-       (when (not content)
-         (error "Internal error: No :content from %S" args)))
-      (when cache
-       (plist-put part :content content)))
-    content))
+  (notmuch--get-bodypart-raw msg part process-crypto nil cache))
 
 ;; Workaround: The call to `mm-display-part' below triggers a bug in
 ;; Emacs 24 if it attempts to use the shr renderer to display an HTML
@@ -926,3 +933,5 @@ status."
 ;; Local Variables:
 ;; byte-compile-warnings: (not cl-functions)
 ;; End:
+
+;;; notmuch-lib.el ends here
index c2f2f4cb127db3d9f2c786b8c8236cffe558193e..bbf61320d75dc4b9da974a801376f5c35ce1477c 100644 (file)
@@ -1,3 +1,5 @@
+;;; notmuch-maildir-fcc.el ---
+
 ;; This file 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 2, or (at your
 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 ;; Boston, MA 02110-1301, USA.
-;;
+
+;;; Commentary:
+
 ;; To use this as the fcc handler for message-mode,
 ;; customize the notmuch-fcc-dirs variable
 
+;;; Code:
+
 (eval-when-compile (require 'cl))
 (require 'message)
 
@@ -211,3 +217,4 @@ return t if successful, and nil otherwise."
 
 (provide 'notmuch-maildir-fcc)
 
+;;; notmuch-maildir-fcc.el ends here
index 914bdd18ed7db29662abadcae3be7e58d32bb49a..d437b8574b925945169cb0b0f393690dcbff4c8e 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-message.el --- message-mode functions specific to notmuch
+;;; notmuch-message.el --- message-mode functions specific to notmuch
 ;;
 ;; Copyright Â© Jesse Rosenthal
 ;;
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: Jesse Rosenthal <jrosenthal@jhu.edu>
 
+;;; Code:
+
 (require 'message)
 (require 'notmuch-tag)
 (require 'notmuch-mua)
@@ -46,3 +48,5 @@ the \"inbox\" and \"todo\" tags, you would set:
 (add-hook 'message-send-hook 'notmuch-message-mark-replied)
 
 (provide 'notmuch-message)
+
+;;; notmuch-message.el ends here
index 57465b205a60a7875a66fdbd39874ca56f2e376c..04459750073c582370d87fa3b743d22dad972e5f 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-mua.el --- emacs style mail-user-agent
+;;; notmuch-mua.el --- emacs style mail-user-agent
 ;;
 ;; Copyright Â© David Edmondson
 ;;
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: David Edmondson <dme@dme.org>
 
+;;; Code:
+
 (require 'message)
 (require 'mm-view)
 (require 'format-spec)
@@ -28,7 +30,9 @@
 
 (eval-when-compile (require 'cl))
 
-(declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth &optional hide))
+(declare-function notmuch-show-insert-body "notmuch-show" (msg body depth))
+(declare-function notmuch-fcc-header-setup "notmuch-maildir-fcc" ())
+(declare-function notmuch-fcc-handler "notmuch-maildir-fcc" (destdir))
 
 ;;
 
@@ -91,6 +95,23 @@ Note that these functions use `mail-citation-hook' if that is non-nil."
   :link '(custom-manual "(message)Insertion Variables")
   :group 'notmuch-reply)
 
+(defcustom notmuch-mua-reply-insert-header-p-function
+  'notmuch-show-reply-insert-header-p-never
+  "Function to decide which parts get a header when replying.
+
+This function specifies which parts of a mime message with
+mutiple parts get a header."
+  :type '(radio (const :tag "No part headers"
+                              notmuch-show-reply-insert-header-p-never)
+               (const :tag "All except multipart/* and hidden parts"
+                              notmuch-show-reply-insert-header-p-trimmed)
+               (const :tag "Only for included text parts"
+                              notmuch-show-reply-insert-header-p-minimal)
+               (const :tag "Exactly as in show view"
+                              notmuch-show-insert-header-p)
+               (function :tag "Other"))
+  :group 'notmuch-reply)
+
 ;;
 
 (defun notmuch-mua-get-switch-function ()
@@ -142,31 +163,6 @@ Note that these functions use `mail-citation-hook' if that is non-nil."
        else if (notmuch-match-content-type (plist-get part :content-type) "multipart/*")
          do (notmuch-mua-reply-crypto (plist-get part :content))))
 
-(defun notmuch-mua-get-quotable-parts (parts)
-  (loop for part in parts
-       if (notmuch-match-content-type (plist-get part :content-type) "multipart/alternative")
-         collect (let* ((subparts (plist-get part :content))
-                       (types (mapcar (lambda (part) (plist-get part :content-type)) subparts))
-                       (chosen-type (car (notmuch-multipart/alternative-choose types))))
-                  (loop for part in (reverse subparts)
-                        if (notmuch-match-content-type (plist-get part :content-type) chosen-type)
-                        return part))
-       else if (notmuch-match-content-type (plist-get part :content-type) "multipart/*")
-         append (notmuch-mua-get-quotable-parts (plist-get part :content))
-       else if (notmuch-match-content-type (plist-get part :content-type) "text/*")
-         collect part))
-
-(defun notmuch-mua-insert-quotable-part (message part)
-  ;; We don't want text properties leaking from the show renderer into
-  ;; the reply so we use a temp buffer. Also we don't want hooks, such
-  ;; as notmuch-wash-*, to be run on the quotable part so we set
-  ;; notmuch-show-insert-text/plain-hook to nil.
-  (insert (with-temp-buffer
-           (let ((notmuch-show-insert-text/plain-hook nil))
-             ;; Show the part but do not add buttons.
-             (notmuch-show-insert-bodypart message part 0 'no-buttons))
-           (buffer-substring-no-properties (point-min) (point-max)))))
-
 ;; There is a bug in emacs 23's message.el that results in a newline
 ;; not being inserted after the References header, so the next header
 ;; is concatenated to the end of it. This function fixes the problem,
@@ -245,10 +241,20 @@ Note that these functions use `mail-citation-hook' if that is non-nil."
        (insert "From: " from "\n")
        (insert "Date: " date "\n\n")
 
-       ;; Get the parts of the original message that should be quoted; this includes
-       ;; all the text parts, except the non-preferred ones in a multipart/alternative.
-       (let ((quotable-parts (notmuch-mua-get-quotable-parts (plist-get original :body))))
-         (mapc (apply-partially 'notmuch-mua-insert-quotable-part original) quotable-parts))
+       (insert (with-temp-buffer
+                 (let
+                     ;; Don't attempt to clean up messages, excerpt
+                     ;; citations, etc. in the original message before
+                     ;; quoting.
+                     ((notmuch-show-insert-text/plain-hook nil)
+                      ;; Don't omit long parts.
+                      (notmuch-show-max-text-part-size 0)
+                      ;; Insert headers for parts as appropriate for replying.
+                      (notmuch-show-insert-header-p-function notmuch-mua-reply-insert-header-p-function)
+                      ;; Don't indent multipart sub-parts.
+                      (notmuch-show-indent-multipart nil))
+                   (notmuch-show-insert-body original (plist-get original :body) 0)
+                   (buffer-substring-no-properties (point-min) (point-max)))))
 
        (set-mark (point))
        (goto-char start)
@@ -269,15 +275,45 @@ Note that these functions use `mail-citation-hook' if that is non-nil."
   (set-buffer-modified-p nil))
 
 (define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]"
-  "Notmuch message composition mode. Mostly like `message-mode'")
+  "Notmuch message composition mode. Mostly like `message-mode'"
+  (when notmuch-address-command
+    (notmuch-address-setup)))
+
+(put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify)
 
 (define-key notmuch-message-mode-map (kbd "C-c C-c") #'notmuch-mua-send-and-exit)
 (define-key notmuch-message-mode-map (kbd "C-c C-s") #'notmuch-mua-send)
 
-(defun notmuch-mua-mail (&optional to subject other-headers &rest other-args)
-  "Invoke the notmuch mail composition window.
-
-OTHER-ARGS are passed through to `message-mail'."
+(defun notmuch-mua-pop-to-buffer (name switch-function)
+  "Pop to buffer NAME, and warn if it already exists and is
+modified. This function is notmuch addaptation of
+`message-pop-to-buffer'."
+  (let ((buffer (get-buffer name)))
+    (if (and buffer
+            (buffer-name buffer))
+       (let ((window (get-buffer-window buffer 0)))
+         (if window
+             ;; Raise the frame already displaying the message buffer.
+             (progn
+               (gnus-select-frame-set-input-focus (window-frame window))
+               (select-window window))
+           (funcall switch-function buffer)
+           (set-buffer buffer))
+         (when (and (buffer-modified-p)
+                    (not (prog1
+                             (y-or-n-p
+                              "Message already being composed; erase? ")
+                           (message nil))))
+           (error "Message being composed")))
+      (funcall switch-function name)
+      (set-buffer name))
+    (erase-buffer)
+    (notmuch-message-mode)))
+
+(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."
   (interactive)
 
   (when notmuch-mua-user-agent-function
@@ -286,11 +322,29 @@ OTHER-ARGS are passed through to `message-mail'."
        (push (cons 'User-Agent user-agent) other-headers))))
 
   (unless (assq 'From other-headers)
-    (push (cons 'From (concat
-                      (notmuch-user-name) " <" (notmuch-user-primary-email) ">")) other-headers))
-
-  (apply #'message-mail to subject other-headers other-args)
-  (notmuch-message-mode)
+    (push (cons 'From (message-make-from
+                      (notmuch-user-name) (notmuch-user-primary-email))) other-headers))
+
+  (notmuch-mua-pop-to-buffer (message-buffer-name "mail" to)
+                            (or switch-function (notmuch-mua-get-switch-function)))
+  (let ((headers
+        (append
+         ;; The following is copied from `message-mail'
+         `((To . ,(or to "")) (Subject . ,(or subject "")))
+         ;; C-h f compose-mail says that headers should be specified as
+         ;; (string . value); however all the rest of message expects
+         ;; headers to be symbols, not strings (eg message-header-format-alist).
+         ;; http://lists.gnu.org/archive/html/emacs-devel/2011-01/msg00337.html
+         ;; We need to convert any string input, eg from rmail-start-mail.
+         (dolist (h other-headers other-headers)
+           (if (stringp (car h)) (setcar h (intern (capitalize (car h))))))))
+       (args (list yank-action send-actions)))
+    ;; message-setup-1 in Emacs 23 does not accept return-action
+    ;; argument. Pass it only if it is supplied by the caller. This
+    ;; will never be the case when we're called by `compose-mail' in
+    ;; Emacs 23.
+    (when return-action (nconc args '(return-action)))
+    (apply 'message-setup-1 headers args))
   (notmuch-fcc-header-setup)
   (message-sort-headers)
   (message-hide-headers)
@@ -343,7 +397,7 @@ the From: header is already filled in by notmuch."
            (ido-completing-read (concat "Sender address for " name ": ") addrs
                                 nil nil nil 'notmuch-mua-sender-history
                                 (car addrs))))
-      (concat name " <" address ">"))))
+      (message-make-from name address))))
 
 (put 'notmuch-mua-new-mail 'notmuch-prefix-doc "... and prompt for sender")
 (defun notmuch-mua-new-mail (&optional prompt-for-sender)
@@ -357,25 +411,53 @@ the From: address first."
           (list (cons 'From (notmuch-mua-prompt-for-sender))))))
     (notmuch-mua-mail nil nil other-headers nil (notmuch-mua-get-switch-function))))
 
-(defun notmuch-mua-new-forward-message (&optional prompt-for-sender)
-  "Invoke the notmuch message forwarding window.
+(defun notmuch-mua-new-forward-messages (messages &optional prompt-for-sender)
+  "Compose a new message forwarding MESSAGES.
 
-The current buffer must contain an RFC2822 message to forward.
-
-If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
-the From: address first."
-  (let* ((cur (current-buffer))
-        (message-forward-decoded-p nil)
-        (subject (message-make-forward-subject))
-        (other-headers
+If PROMPT-FOR-SENDER is non-nil, the user will be prompteed for
+the From: address."
+  (let* ((other-headers
          (when (or prompt-for-sender notmuch-always-prompt-for-sender)
-           (list (cons 'From (notmuch-mua-prompt-for-sender))))))
-    (notmuch-mua-mail nil subject other-headers nil (notmuch-mua-get-switch-function))
-    (message-forward-make-body cur)
-    ;; `message-forward-make-body' shows the User-agent header.  Hide
-    ;; it again.
-    (message-hide-headers)
-    (set-buffer-modified-p nil)))
+           (list (cons 'From (notmuch-mua-prompt-for-sender)))))
+        forward-subject) ;; Comes from the first message and is
+                         ;; applied later.
+
+    ;; Generate the template for the outgoing message.
+    (notmuch-mua-mail nil "" other-headers nil (notmuch-mua-get-switch-function))
+
+    (save-excursion
+      ;; Insert all of the forwarded messages.
+      (mapc (lambda (id)
+             (let ((temp-buffer (get-buffer-create
+                                 (concat "*notmuch-fwd-raw-" id "*"))))
+               ;; Get the raw version of this message in the buffer.
+               (with-current-buffer temp-buffer
+                 (erase-buffer)
+                 (let ((coding-system-for-read 'no-conversion))
+                   (call-process notmuch-command nil t nil "show" "--format=raw" id))
+                 ;; Because we process the messages in reverse order,
+                 ;; always generate a forwarded subject, then use the
+                 ;; last (i.e. first) one.
+                 (setq forward-subject (message-make-forward-subject)))
+               ;; Make a copy ready to be forwarded in the
+               ;; composition buffer.
+               (message-forward-make-body temp-buffer)
+               ;; Kill the temporary buffer.
+               (kill-buffer temp-buffer)))
+           ;; `message-forward-make-body' always puts the message at
+           ;; the top, so do them in reverse order.
+           (reverse messages))
+
+      ;; Add in the appropriate subject.
+      (save-restriction
+       (message-narrow-to-headers)
+       (message-remove-header "Subject")
+       (message-add-header (concat "Subject: " forward-subject)))
+
+      ;; `message-forward-make-body' shows the User-agent header.  Hide
+      ;; it again.
+      (message-hide-headers)
+      (set-buffer-modified-p nil))))
 
 (defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all)
   "Compose a reply to the message identified by QUERY-STRING.
@@ -435,3 +517,5 @@ simply runs the corresponding `message-mode' hook functions."
 ;;
 
 (provide 'notmuch-mua)
+
+;;; notmuch-mua.el ends here
index d59c0e1cc22727d60454ac0a79a04f63a99cf21d..620ca89ddc5edb6e20623c9d691885ea0c0318b5 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-parser.el --- streaming S-expression parser
+;;; notmuch-parser.el --- streaming S-expression parser
 ;;
 ;; Copyright Â© Austin Clements
 ;;
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: Austin Clements <aclements@csail.mit.edu>
 
+;;; Code:
+
 (require 'cl)
 
 (defun notmuch-sexp-create-parser ()
@@ -205,3 +207,5 @@ move point in the input buffer."
 ;; Local Variables:
 ;; byte-compile-warnings: (not cl-functions)
 ;; End:
+
+;;; notmuch-parser.el ends here
index 8c18f4bd63da4d0fa9a1d30c6272c8667f671bdb..480a0cfee487537c6197419f3b9645cafee24d2b 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-print.el --- printing messages from notmuch.
+;;; notmuch-print.el --- printing messages from notmuch.
 ;;
 ;; Copyright Â© David Edmondson
 ;;
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: David Edmondson <dme@dme.org>
 
+;;; Code:
+
 (require 'notmuch-lib)
 
 (declare-function notmuch-show-get-prop "notmuch-show" (prop &optional props))
@@ -90,3 +92,5 @@ Optional OUTPUT allows passing a list of flags to muttprint."
   (funcall notmuch-print-mechanism msg))
 
 (provide 'notmuch-print)
+
+;;; notmuch-print.el ends here
index d1daffce4eaa3562999c5c345637151ec3eaf822..8587d8818cf759f4f3129e48072a8b1c98ea90f1 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-query.el --- provide an emacs api to query notmuch
+;;; notmuch-query.el --- provide an emacs api to query notmuch
 ;;
 ;; Copyright Â© David Bremner
 ;;
@@ -19,6 +19,8 @@
 ;;
 ;; Authors: David Bremner <david@tethera.net>
 
+;;; Code:
+
 (require 'notmuch-lib)
 
 (defun notmuch-query-get-threads (search-terms)
@@ -74,3 +76,5 @@ See the function notmuch-query-get-threads for more information."
    (notmuch-query-get-threads search-terms)))
 
 (provide 'notmuch-query)
+
+;;; notmuch-query.el ends here
index 49fd198a7a5cc365b09c26b378b8f1f5e5aceeb2..5d9b7b45c557b42c0ad540ebdc103b9e006d7394 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-show.el --- displaying notmuch forests.
+;;; notmuch-show.el --- displaying notmuch forests.
 ;;
 ;; Copyright Â© Carl Worth
 ;; Copyright Â© David Edmondson
@@ -21,6 +21,8 @@
 ;; Authors: Carl Worth <cworth@cworth.org>
 ;;          David Edmondson <dme@dme.org>
 
+;;; Code:
+
 (eval-when-compile (require 'cl))
 (require 'mm-view)
 (require 'message)
@@ -153,27 +155,21 @@ indentation."
 
 (defvar notmuch-show-thread-id nil)
 (make-variable-buffer-local 'notmuch-show-thread-id)
-(put 'notmuch-show-thread-id 'permanent-local t)
 
 (defvar notmuch-show-parent-buffer nil)
 (make-variable-buffer-local 'notmuch-show-parent-buffer)
-(put 'notmuch-show-parent-buffer 'permanent-local t)
 
 (defvar notmuch-show-query-context nil)
 (make-variable-buffer-local 'notmuch-show-query-context)
-(put 'notmuch-show-query-context 'permanent-local t)
 
 (defvar notmuch-show-process-crypto nil)
 (make-variable-buffer-local 'notmuch-show-process-crypto)
-(put 'notmuch-show-process-crypto 'permanent-local t)
 
 (defvar notmuch-show-elide-non-matching-messages nil)
 (make-variable-buffer-local 'notmuch-show-elide-non-matching-messages)
-(put 'notmuch-show-elide-non-matching-messages 'permanent-local t)
 
 (defvar notmuch-show-indent-content t)
 (make-variable-buffer-local 'notmuch-show-indent-content)
-(put 'notmuch-show-indent-content 'permanent-local t)
 
 (defvar notmuch-show-attachment-debug nil
   "If t log stdout and stderr from attachment handlers
@@ -353,8 +349,6 @@ operation on the contents of the current buffer."
                'message-header-cc)
               ((looking-at "[Ss]ubject:")
                'message-header-subject)
-              ((looking-at "[Ff]rom:")
-               'message-header-from)
               (t
                'message-header-other))))
 
@@ -509,36 +503,37 @@ message at DEPTH in the current thread."
 
 (defun notmuch-show-toggle-part-invisibility (&optional button)
   (interactive)
-  (let* ((button (or button (button-at (point))))
-        (overlay (button-get button 'overlay))
-        (lazy-part (button-get button :notmuch-lazy-part)))
-    ;; We have a part to toggle if there is an overlay or if there is a lazy part.
-    ;; If neither is present we cannot toggle the part so we just return nil.
-    (when (or overlay lazy-part)
-      (let* ((show (button-get button :notmuch-part-hidden))
-            (new-start (button-start button))
-            (button-label (button-get button :base-label))
-            (old-point (point))
-            (properties (text-properties-at (button-start button)))
-            (inhibit-read-only t))
-       ;; Toggle the button itself.
-       (button-put button :notmuch-part-hidden (not show))
-       (goto-char new-start)
-       (insert "[ " button-label (if show " ]" " (hidden) ]"))
-       (set-text-properties new-start (point) properties)
-       (let ((old-end (button-end button)))
-         (move-overlay button new-start (point))
-         (delete-region (point) old-end))
-       (goto-char (min old-point (1- (button-end button))))
-       ;; Return nil if there is a lazy-part, it is empty, and we are
-       ;; trying to show it.  In all other cases return t.
-       (if lazy-part
-           (when show
-             (button-put button :notmuch-lazy-part nil)
-             (notmuch-show-lazy-part lazy-part button))
-         ;; else there must be an overlay.
-         (overlay-put overlay 'invisible (not show))
-         t)))))
+  (let ((button (or button (button-at (point)))))
+    (when button
+      (let ((overlay (button-get button 'overlay))
+           (lazy-part (button-get button :notmuch-lazy-part)))
+       ;; We have a part to toggle if there is an overlay or if there is a lazy part.
+       ;; If neither is present we cannot toggle the part so we just return nil.
+       (when (or overlay lazy-part)
+         (let* ((show (button-get button :notmuch-part-hidden))
+                (new-start (button-start button))
+                (button-label (button-get button :base-label))
+                (old-point (point))
+                (properties (text-properties-at (button-start button)))
+                (inhibit-read-only t))
+           ;; Toggle the button itself.
+           (button-put button :notmuch-part-hidden (not show))
+           (goto-char new-start)
+           (insert "[ " button-label (if show " ]" " (hidden) ]"))
+           (set-text-properties new-start (point) properties)
+           (let ((old-end (button-end button)))
+             (move-overlay button new-start (point))
+             (delete-region (point) old-end))
+           (goto-char (min old-point (1- (button-end button))))
+           ;; Return nil if there is a lazy-part, it is empty, and we are
+           ;; trying to show it.  In all other cases return t.
+           (if lazy-part
+               (when show
+                 (button-put button :notmuch-lazy-part nil)
+                 (notmuch-show-lazy-part lazy-part button))
+             ;; else there must be an overlay.
+             (overlay-put overlay 'invisible (not show))
+             t)))))))
 
 ;; Part content ID handling
 
@@ -614,7 +609,7 @@ will return nil if the CID is unknown or cannot be retrieved."
          (plist-get part :content)))
 
 (defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth button)
-  (let ((chosen-type (car (notmuch-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
+  (let ((chosen-type (car (notmuch-multipart/alternative-choose msg (notmuch-show-multipart/*-to-list part))))
        (inner-parts (plist-get part :content))
        (start (point)))
     ;; This inserts all parts of the chosen type rather than just one,
@@ -647,14 +642,12 @@ will return nil if the CID is unknown or cannot be retrieved."
   t)
 
 (defun notmuch-show-insert-part-multipart/signed (msg part content-type nth depth button)
-  (button-put button 'face 'notmuch-crypto-part-header)
-  ;; add signature status button if sigstatus provided
-  (if (plist-member part :sigstatus)
-      (let* ((from (notmuch-show-get-header :From msg))
-            (sigstatus (car (plist-get part :sigstatus))))
-       (notmuch-crypto-insert-sigstatus-button sigstatus from))
-    ;; if we're not adding sigstatus, tell the user how they can get it
-    (button-put button 'help-echo "Set notmuch-crypto-process-mime to process cryptographic MIME parts."))
+  (when button
+    (button-put button 'face 'notmuch-crypto-part-header))
+
+  ;; Insert a button detailing the signature status.
+  (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus))
+                                         (notmuch-show-get-header :From msg))
 
   (let ((inner-parts (plist-get part :content))
        (start (point)))
@@ -668,18 +661,15 @@ will return nil if the CID is unknown or cannot be retrieved."
   t)
 
 (defun notmuch-show-insert-part-multipart/encrypted (msg part content-type nth depth button)
-  (button-put button 'face 'notmuch-crypto-part-header)
-  ;; add encryption status button if encstatus specified
-  (if (plist-member part :encstatus)
-      (let ((encstatus (car (plist-get part :encstatus))))
-       (notmuch-crypto-insert-encstatus-button encstatus)
-       ;; add signature status button if sigstatus specified
-       (if (plist-member part :sigstatus)
-           (let* ((from (notmuch-show-get-header :From msg))
-                  (sigstatus (car (plist-get part :sigstatus))))
-             (notmuch-crypto-insert-sigstatus-button sigstatus from))))
-    ;; if we're not adding encstatus, tell the user how they can get it
-    (button-put button 'help-echo "Set notmuch-crypto-process-mime to process cryptographic MIME parts."))
+  (when button
+    (button-put button 'face 'notmuch-crypto-part-header))
+
+  ;; Insert a button detailing the encryption status.
+  (notmuch-crypto-insert-encstatus-button (car (plist-get part :encstatus)))
+
+  ;; Insert a button detailing the signature status.
+  (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus))
+                                         (notmuch-show-get-header :From msg))
 
   (let ((inner-parts (plist-get part :content))
        (start (point)))
@@ -848,21 +838,16 @@ will return nil if the CID is unknown or cannot be retrieved."
 ;; \f
 
 (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth button)
-  (let ((handlers (notmuch-show-handlers-for content-type)))
-    ;; Run the content handlers until one of them returns a non-nil
-    ;; value.
-    (while (and handlers
-               (not (condition-case err
-                        (funcall (car handlers) msg part content-type nth depth button)
-                      ;; Specifying `debug' here lets the debugger
-                      ;; run if `debug-on-error' is non-nil.
-                      ((debug error)
-                       (progn
-                               (insert "!!! Bodypart insert error: ")
-                               (insert (error-message-string err))
-                               (insert " !!!\n") nil)))))
-      (setq handlers (cdr handlers))))
-  t)
+  ;; Run the handlers until one of them succeeds.
+  (loop for handler in (notmuch-show-handlers-for content-type)
+       until (condition-case err
+                 (funcall handler msg part content-type nth depth button)
+               ;; Specifying `debug' here lets the debugger run if
+               ;; `debug-on-error' is non-nil.
+               ((debug error)
+                (insert "!!! Bodypart handler `" (prin1-to-string handler) "' threw an error:\n"
+                        "!!! " (error-message-string err) "\n")
+                nil))))
 
 (defun notmuch-show-create-part-overlays (button beg end)
   "Add an overlay to the part between BEG and END"
@@ -929,34 +914,62 @@ will return nil if the CID is unknown or cannot be retrieved."
       ;; showable this returns nil.
       (notmuch-show-create-part-overlays button part-beg part-end))))
 
+(defun notmuch-show-mime-type (part)
+  "Return the correct mime-type to use for PART."
+  (let ((content-type (downcase (plist-get part :content-type))))
+    (or (and (string= content-type "application/octet-stream")
+            (notmuch-show-get-mime-type-of-application/octet-stream part))
+       (and (string= content-type "inline patch")
+            "text/x-diff")
+       content-type)))
+
+;; The following variable can be overridden by let bindings.
+(defvar notmuch-show-insert-header-p-function 'notmuch-show-insert-header-p
+  "Specify which function decides which part headers get inserted.
+
+The function should take two parameters, PART and HIDE, and
+should return non-NIL if a header button should be inserted for
+this part.")
+
+(defun notmuch-show-insert-header-p (part hide)
+  ;; Show all part buttons except for the first part if it is text/plain.
+  (let ((mime-type (notmuch-show-mime-type part)))
+    (not (and (string= mime-type "text/plain")
+             (<= (plist-get part :id) 1)))))
+
+(defun notmuch-show-reply-insert-header-p-never (part hide)
+  nil)
+
+(defun notmuch-show-reply-insert-header-p-trimmed (part hide)
+  (let ((mime-type (notmuch-show-mime-type part)))
+    (and (not (notmuch-match-content-type mime-type "multipart/*"))
+        (not hide))))
+
+(defun notmuch-show-reply-insert-header-p-minimal (part hide)
+  (let ((mime-type (notmuch-show-mime-type part)))
+    (and (notmuch-match-content-type mime-type "text/*")
+        (not hide))))
+
 (defun notmuch-show-insert-bodypart (msg part depth &optional hide)
   "Insert the body part PART at depth DEPTH in the current thread.
 
 HIDE determines whether to show or hide the part and the button
 as follows: If HIDE is nil, show the part and the button. If HIDE
-is t, hide the part initially and show the button. If HIDE is
-'no-buttons, show the part but do not add any buttons (this is
-useful for quoting in replies)."
+is t, hide the part initially and show the button."
 
   (let* ((content-type (downcase (plist-get part :content-type)))
-        (mime-type (or (and (string= content-type "application/octet-stream")
-                            (notmuch-show-get-mime-type-of-application/octet-stream part))
-                       (and (string= content-type "inline patch")
-                            "text/x-diff")
-                       content-type))
+        (mime-type (notmuch-show-mime-type part))
         (nth (plist-get part :id))
         (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)))
         (beg (point))
-        ;; We omit the part button for the first (or only) part if
-        ;; this is text/plain, or HIDE is 'no-buttons.
-        (button (unless (or (equal hide 'no-buttons)
-                            (and (string= mime-type "text/plain") (<= nth 1)))
+        ;; This default header-p function omits the part button for
+        ;; the first (or only) part if this is text/plain.
+        (button (when (funcall notmuch-show-insert-header-p-function part hide)
                   (notmuch-show-insert-part-header nth mime-type content-type (plist-get part :filename))))
         ;; Hide the part initially if HIDE is t, or if it is too long
-        ;; and we have a button to allow toggling (thus reply which
-        ;; uses 'no-buttons automatically includes long parts)
+        ;; and we have a button to allow toggling.
         (show-part (not (or (equal hide t)
                             (and long button))))
         (content-beg (point)))
@@ -966,8 +979,9 @@ useful for quoting in replies)."
 
     (if show-part
         (notmuch-show-insert-bodypart-internal msg part mime-type nth depth button)
-      (button-put button :notmuch-lazy-part
-                  (list msg part mime-type nth depth button)))
+      (when button
+       (button-put button :notmuch-lazy-part
+                   (list msg part mime-type nth depth button))))
 
     ;; Some of the body part handlers leave point somewhere up in the
     ;; part, so we make sure that we're down at the end.
@@ -1199,71 +1213,101 @@ non-nil.
 The optional BUFFER-NAME provides the name of the buffer in
 which the message thread is shown. If it is nil (which occurs
 when the command is called interactively) the argument to the
-function is used."
+function is used.
+
+Returns the buffer containing the messages, or NIL if no messages
+matched."
   (interactive "sNotmuch show: \nP")
   (let ((buffer-name (generate-new-buffer-name
                      (or buffer-name
                          (concat "*notmuch-" thread-id "*")))))
     (switch-to-buffer (get-buffer-create buffer-name))
-    ;; Set the default value for `notmuch-show-process-crypto' in this
-    ;; buffer.
-    (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
-    ;; Set the default value for
-    ;; `notmuch-show-elide-non-matching-messages' in this buffer. If
-    ;; elide-toggle is set, invert the default.
-    (setq notmuch-show-elide-non-matching-messages notmuch-show-only-matching-messages)
-    (if elide-toggle
-       (setq notmuch-show-elide-non-matching-messages (not notmuch-show-elide-non-matching-messages)))
+    ;; No need to track undo information for this buffer.
+    (setq buffer-undo-list t)
+
+    (notmuch-show-mode)
 
+    ;; Set various buffer local variables to their appropriate initial
+    ;; state. Do this after enabling `notmuch-show-mode' so that they
+    ;; aren't wiped out.
     (setq notmuch-show-thread-id thread-id
          notmuch-show-parent-buffer parent-buffer
-         notmuch-show-query-context query-context)
-    (notmuch-show-build-buffer)
-    (notmuch-show-goto-first-wanted-message)
-    (current-buffer)))
+         notmuch-show-query-context query-context
 
-(defun notmuch-show-build-buffer ()
-  (let ((inhibit-read-only t))
+         notmuch-show-process-crypto notmuch-crypto-process-mime
+         ;; If `elide-toggle', invert the default value.
+         notmuch-show-elide-non-matching-messages
+         (if elide-toggle
+             (not notmuch-show-only-matching-messages)
+           notmuch-show-only-matching-messages))
 
-    (notmuch-show-mode)
     (add-hook 'post-command-hook #'notmuch-show-command-hook nil t)
-
-    ;; Don't track undo information for this buffer
-    (set 'buffer-undo-list t)
+    (jit-lock-register #'notmuch-show-buttonise-links)
 
     (notmuch-tag-clear-cache)
-    (erase-buffer)
-    (goto-char (point-min))
-    (save-excursion
-      (let* ((basic-args (list notmuch-show-thread-id))
-            (args (if notmuch-show-query-context
-                      (append (list "\'") basic-args
-                              (list "and (" notmuch-show-query-context ")\'"))
-                    (append (list "\'") basic-args (list "\'"))))
-            (cli-args (cons "--exclude=false"
-                            (when notmuch-show-elide-non-matching-messages
-                              (list "--entire-thread=false")))))
-
-       (notmuch-show-insert-forest (notmuch-query-get-threads (append cli-args args)))
-       ;; If the query context reduced the results to nothing, run
-       ;; the basic query.
-       (when (and (eq (buffer-size) 0)
-                  notmuch-show-query-context)
-         (notmuch-show-insert-forest
-          (notmuch-query-get-threads (append cli-args basic-args)))))
-
-      (jit-lock-register #'notmuch-show-buttonise-links)
-
-      (notmuch-show-mapc (lambda () (notmuch-show-set-prop :orig-tags (notmuch-show-get-tags))))
+
+    (let ((inhibit-read-only t))
+      (if (notmuch-show--build-buffer)
+         ;; Messages were inserted into the buffer.
+         (current-buffer)
+
+       ;; No messages were inserted - presumably none matched the
+       ;; query.
+       (kill-buffer (current-buffer))
+       (ding)
+       (message "No messages matched the query!")
+       nil))))
+
+(defun notmuch-show--build-buffer (&optional state)
+  "Display messages matching the current buffer context.
+
+Apply the previously saved STATE if supplied, otherwise show the
+first relevant message.
+
+If no messages match the query return NIL."
+  (let* ((basic-args (list notmuch-show-thread-id))
+        (args (if notmuch-show-query-context
+                  (append (list "\'") basic-args
+                          (list "and (" notmuch-show-query-context ")\'"))
+                (append (list "\'") basic-args (list "\'"))))
+        (cli-args (cons "--exclude=false"
+                        (when notmuch-show-elide-non-matching-messages
+                          (list "--entire-thread=false"))))
+
+        (forest (or (notmuch-query-get-threads (append cli-args args))
+                    ;; If a query context reduced the number of
+                    ;; results to zero, try again without it.
+                    (and notmuch-show-query-context
+                         (notmuch-query-get-threads (append cli-args basic-args)))))
+
+        ;; Must be reset every time we are going to start inserting
+        ;; messages into the buffer.
+        (notmuch-show-previous-subject ""))
+
+    (when forest
+      (notmuch-show-insert-forest forest)
+
+      ;; Store the original tags for each message so that we can
+      ;; 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.
       (setq header-line-format
            (replace-regexp-in-string "%" "%%"
-                           (notmuch-sanitize
-                            (notmuch-show-strip-re
-                             (notmuch-show-get-subject)))))
+                                     (notmuch-sanitize
+                                      (notmuch-show-strip-re
+                                       (notmuch-show-get-subject)))))
+
+      (run-hooks 'notmuch-show-hook)
+
+      (if state
+         (notmuch-show-apply-state state)
+       ;; With no state to apply, just go to the first message.
+       (notmuch-show-goto-first-wanted-message)))
 
-      (run-hooks 'notmuch-show-hook))))
+    ;; Report back to the caller whether any messages matched.
+    forest))
 
 (defun notmuch-show-capture-state ()
   "Capture the state of the current buffer.
@@ -1322,17 +1366,17 @@ reset based on the original query."
   (let ((inhibit-read-only t)
        (state (unless reset-state
                 (notmuch-show-capture-state))))
-    ;; erase-buffer does not seem to remove overlays, which can lead
+    ;; `erase-buffer' does not seem to remove overlays, which can lead
     ;; to weird effects such as remaining images, so remove them
     ;; manually.
     (remove-overlays)
     (erase-buffer)
-    (notmuch-show-build-buffer)
-    (if state
-       (notmuch-show-apply-state state)
-      ;; We're resetting state, so navigate to the first open message
-      ;; and mark it read, just like opening a new show buffer.
-      (notmuch-show-goto-first-wanted-message))))
+
+    (unless (notmuch-show--build-buffer state)
+      ;; No messages were inserted.
+      (kill-buffer (current-buffer))
+      (ding)
+      (message "Refreshing the buffer resulted in no messages!"))))
 
 (defvar notmuch-show-stash-map
   (let ((map (make-sparse-keymap)))
@@ -1373,6 +1417,7 @@ reset based on the original query."
     (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
     (define-key map (kbd "TAB") 'notmuch-show-next-button)
     (define-key map "f" 'notmuch-show-forward-message)
+    (define-key map "F" 'notmuch-show-forward-open-messages)
     (define-key map "l" 'notmuch-show-filter-thread)
     (define-key map "r" 'notmuch-show-reply-sender)
     (define-key map "R" 'notmuch-show-reply)
@@ -1797,8 +1842,18 @@ any effects from previous calls to
 (defun notmuch-show-forward-message (&optional prompt-for-sender)
   "Forward the current message."
   (interactive "P")
-  (with-current-notmuch-show-message
-   (notmuch-mua-new-forward-message prompt-for-sender)))
+  (notmuch-mua-new-forward-messages (list (notmuch-show-get-message-id))
+                                   prompt-for-sender))
+
+(put 'notmuch-show-forward-open-messages 'notmuch-prefix-doc
+     "... and prompt for sender")
+(defun notmuch-show-forward-open-messages (&optional prompt-for-sender)
+  "Forward the currently open messages."
+  (interactive "P")
+  (let ((open-messages (notmuch-show-get-message-ids-for-open-messages)))
+    (unless open-messages
+      (error "No open messages to forward."))
+    (notmuch-mua-new-forward-messages open-messages prompt-for-sender)))
 
 (defun notmuch-show-next-message (&optional pop-at-end)
   "Show the next message.
@@ -1880,12 +1935,15 @@ 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 "*"))))
-    (let ((coding-system-for-read 'no-conversion))
-      (call-process notmuch-command nil buf nil "show" "--format=raw" id))
+        (buf (get-buffer-create (concat "*notmuch-raw-" id "*")))
+        (inhibit-read-only t))
     (switch-to-buffer buf)
+    (erase-buffer)
+    (let ((coding-system-for-read 'no-conversion))
+      (call-process notmuch-command nil t nil "show" "--format=raw" id))
     (goto-char (point-min))
     (set-buffer-modified-p nil)
+    (setq buffer-read-only t)
     (view-buffer buf 'kill-buffer-if-not-modified)))
 
 (put 'notmuch-show-pipe-message 'notmuch-doc
@@ -2323,3 +2381,5 @@ is destroyed when FN returns."
 
 
 (provide 'notmuch-show)
+
+;;; notmuch-show.el ends here
index c7f62c90974995caf64aea212e0a5eae9fa97b5e..98064a3b6f64378679689840d435ffc10b6cf657 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-tag.el --- tag messages within emacs
+;;; notmuch-tag.el --- tag messages within emacs
 ;;
 ;; Copyright Â© Damien Cassou
 ;; Copyright Â© Carl Worth
index 384cb76bcf3c086f6f727112334e0ad4bea1eb84..4f9ca2deecf44e6f649898b32a4615946dcab6d9 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-tree.el --- displaying notmuch forests.
+;;; notmuch-tree.el --- displaying notmuch forests.
 ;;
 ;; Copyright Â© Carl Worth
 ;; Copyright Â© David Edmondson
@@ -22,6 +22,8 @@
 ;; Authors: David Edmondson <dme@dme.org>
 ;;          Mark Walters <markwalters1009@gmail.com>
 
+;;; Code:
+
 (require 'mail-parse)
 
 (require 'notmuch-lib)
@@ -945,3 +947,5 @@ The arguments are:
 ;;
 
 (provide 'notmuch-tree)
+
+;;; notmuch-tree.el ends here
index 236aaf7dce38a46d4a1bcb193d1b0a64762a61c6..88cc01ce1378f03f0c44afaeb00533075436181e 100644 (file)
@@ -1,3 +1,4 @@
+;;; notmuch-version.el --- Version of notmuch
 ;; -*- emacs-lisp -*-
 ;;
 ;; %AG%
 ;; You should have received a copy of the GNU General Public License
 ;; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
 
+;;; Code:
+
 (defconst notmuch-emacs-version %VERSION%
   "Version of Notmuch Emacs MUA.")
 
 (provide 'notmuch-version)
+
+;;; notmuch-version.el ends here
index a76b4f5b589ab0d0f1ada7ced2bc024e31a5b91b..065af16fb071baeb9cd53e1b43ffec85f2beca86 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-wash.el --- cleaning up message bodies
+;;; notmuch-wash.el --- cleaning up message bodies
 ;;
 ;; Copyright Â© Carl Worth
 ;; Copyright Â© David Edmondson
@@ -21,6 +21,8 @@
 ;; Authors: Carl Worth <cworth@cworth.org>
 ;;          David Edmondson <dme@dme.org>
 
+;;; Code:
+
 (require 'coolj)
 
 (declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth &optional hide))
@@ -423,3 +425,5 @@ for error."
 ;;
 
 (provide 'notmuch-wash)
+
+;;; notmuch-wash.el ends here
index 463b9262212ecad69b7cff10420359b3c071888d..a8a4c8e56c2a16b9d9f2c303fa8831bc0232208d 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch.el --- run notmuch within emacs
+;;; notmuch.el --- run notmuch within emacs
 ;;
 ;; Copyright Â© Carl Worth
 ;;
@@ -18,6 +18,9 @@
 ;; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
 ;;
 ;; Authors: Carl Worth <cworth@cworth.org>
+;; Homepage: https://notmuchmail.org/
+
+;;; Commentary:
 
 ;; This is an emacs-based interface to the notmuch mail system.
 ;;
@@ -47,6 +50,8 @@
 ;; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
 ;; required, but is available from http://notmuchmail.org).
 
+;;; Code:
+
 (eval-when-compile (require 'cl))
 (require 'mm-view)
 (require 'message)
@@ -162,7 +167,7 @@ there will be called at other points of notmuch execution."
     (define-key map "o" 'notmuch-search-toggle-order)
     (define-key map "c" 'notmuch-search-stash-map)
     (define-key map "t" 'notmuch-search-filter-by-tag)
-    (define-key map "f" 'notmuch-search-filter)
+    (define-key map "l" 'notmuch-search-filter)
     (define-key map [mouse-1] 'notmuch-search-show-thread)
     (define-key map "*" 'notmuch-search-tag-all)
     (define-key map "a" 'notmuch-search-archive-thread)
@@ -458,7 +463,11 @@ no messages in the region then return nil."
   (notmuch-search-properties-in-region :subject beg end))
 
 (defun notmuch-search-show-thread (&optional elide-toggle)
-  "Display the currently selected thread."
+  "Display the currently selected thread.
+
+With a prefix argument, invert the default value of
+`notmuch-show-only-matching-messages' when displaying the
+thread."
   (interactive "P")
   (let ((thread-id (notmuch-search-find-thread-id))
        (subject (notmuch-search-find-subject)))
@@ -988,7 +997,7 @@ Enclose QUERY-STRING in parentheses if it matches
     query-string))
 
 (defun notmuch-search-filter (query)
-  "Filter the current search results based on an additional query string.
+  "Filter or LIMIT the current search results based on an additional query string.
 
 Runs a new search matching only messages that match both the
 current search results AND the additional query string provided."
@@ -1060,3 +1069,5 @@ notmuch buffers exist, run `notmuch'."
     (let ((init-file (locate-file notmuch-init-file '("/")
                                  (get-load-suffixes))))
       (if init-file (load init-file nil t t))))
+
+;;; notmuch.el ends here
index 5e86955d8dfd91b3880a5d53217b9265eb69c65e..c8c5e26106ad9d1086eb92ee0998d30a312dca99 100644 (file)
@@ -1635,6 +1635,9 @@ notmuch_database_begin_atomic (notmuch_database_t *notmuch)
        notmuch->atomic_nesting > 0)
        goto DONE;
 
+       if (notmuch_database_needs_upgrade(notmuch))
+               return NOTMUCH_STATUS_UPGRADE_REQUIRED;
+
     try {
        (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false);
     } catch (const Xapian::Error &error) {
@@ -1758,18 +1761,11 @@ _notmuch_database_split_path (void *ctx,
     slash = path + strlen (path) - 1;
 
     /* First, skip trailing slashes. */
-    while (slash != path) {
-       if (*slash != '/')
-           break;
-
+    while (slash != path && *slash == '/')
        --slash;
-    }
 
     /* Then, find a slash. */
-    while (slash != path) {
-       if (*slash == '/')
-           break;
-
+    while (slash != path && *slash != '/') {
        if (basename)
            *basename = slash;
 
@@ -1777,12 +1773,8 @@ _notmuch_database_split_path (void *ctx,
     }
 
     /* Finally, skip multiple slashes. */
-    while (slash != path) {
-       if (*slash != '/')
-           break;
-
+    while (slash != path && *(slash - 1) == '/')
        --slash;
-    }
 
     if (slash == path) {
        if (directory)
@@ -1791,7 +1783,7 @@ _notmuch_database_split_path (void *ctx,
            *basename = path;
     } else {
        if (directory)
-           *directory = talloc_strndup (ctx, path, slash - path + 1);
+           *directory = talloc_strndup (ctx, path, slash - path);
     }
 
     return NOTMUCH_STATUS_SUCCESS;
index e81aa81902883b56f30b01e0aafa2476d8622737..f166aefd2fc1f6f6c5ea1e306e4fc033ead09c59 100644 (file)
@@ -377,7 +377,8 @@ _index_mime_part (notmuch_message_t *message,
 
     disposition = g_mime_object_get_content_disposition (part);
     if (disposition &&
-       strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+       strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                   GMIME_DISPOSITION_ATTACHMENT) == 0)
     {
        const char *filename = g_mime_part_get_filename (GMIME_PART (part));
 
index 26b5e76e9636b67e43ca5c42c08210f73c70f0c2..68393055b3eb73b73ee3a1a4b5bde38ce8d94c73 100644 (file)
@@ -728,7 +728,7 @@ _notmuch_message_add_filename (notmuch_message_t *message,
  * Note: This function does not remove a document from the database,
  * even if the specified filename is the only filename for this
  * message. For that functionality, see
- * _notmuch_database_remove_message. */
+ * notmuch_database_remove_message. */
 notmuch_status_t
 _notmuch_message_remove_filename (notmuch_message_t *message,
                                  const char *filename)
@@ -1037,20 +1037,90 @@ _notmuch_message_sync (notmuch_message_t *message)
     message->modified = FALSE;
 }
 
-/* Delete a message document from the database. */
+/* Delete a message document from the database, leaving a ghost
+ * message in its place */
 notmuch_status_t
 _notmuch_message_delete (notmuch_message_t *message)
 {
     notmuch_status_t status;
     Xapian::WritableDatabase *db;
+    const char *mid, *tid, *query_string;
+    notmuch_message_t *ghost;
+    notmuch_private_status_t private_status;
+    notmuch_database_t *notmuch;
+    notmuch_query_t *query;
+    unsigned int count = 0;
+    notmuch_bool_t is_ghost;
+
+    mid = notmuch_message_get_message_id (message);
+    tid = notmuch_message_get_thread_id (message);
+    notmuch = message->notmuch;
 
     status = _notmuch_database_ensure_writable (message->notmuch);
     if (status)
        return status;
 
-    db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
     db->delete_document (message->doc_id);
-    return NOTMUCH_STATUS_SUCCESS;
+
+    /* if this was a ghost to begin with, we are done */
+    private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost);
+    if (private_status)
+       return COERCE_STATUS (private_status,
+                             "Error trying to determine whether message was a ghost");
+    if (is_ghost)
+       return NOTMUCH_STATUS_SUCCESS;
+
+    query_string = talloc_asprintf (message, "thread:%s", tid);
+    query = notmuch_query_create (notmuch, query_string);
+    if (query == NULL)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+    status = notmuch_query_count_messages_st (query, &count);
+    if (status) {
+       notmuch_query_destroy (query);
+       return status;
+    }
+
+    if (count > 0) {
+       /* reintroduce a ghost in its place because there are still
+        * other active messages in this thread: */
+       ghost = _notmuch_message_create_for_message_id (notmuch, mid, &private_status);
+       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+           private_status = _notmuch_message_initialize_ghost (ghost, tid);
+           if (! private_status)
+               _notmuch_message_sync (ghost);
+       } else if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
+           /* this is deeply weird, and we should not have gotten
+              into this state.  is there a better error message to
+              return here? */
+           status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+       }
+
+       notmuch_message_destroy (ghost);
+       status = COERCE_STATUS (private_status, "Error converting to ghost message");
+    } else {
+       /* the thread is empty; drop all ghost messages from it */
+       notmuch_messages_t *messages;
+       status = _notmuch_query_search_documents (query,
+                                                 "ghost",
+                                                 &messages);
+       if (status == NOTMUCH_STATUS_SUCCESS) {
+           notmuch_status_t last_error = NOTMUCH_STATUS_SUCCESS;
+           while (notmuch_messages_valid (messages)) {
+               message = notmuch_messages_get (messages);
+               status = _notmuch_message_delete (message);
+               if (status) /* we'll report the last failure we see;
+                            * if there is more than one failure, we
+                            * forget about previous ones */
+                   last_error = status;
+               notmuch_message_destroy (message);
+               notmuch_messages_move_to_next (messages);
+           }
+           status = last_error;
+       }
+    }
+    notmuch_query_destroy (query);
+    return status;
 }
 
 /* Transform a blank message into a ghost message.  The caller must
@@ -1180,7 +1250,7 @@ _notmuch_message_remove_term (notmuch_message_t *message,
        message->doc.remove_term (term);
        message->modified = TRUE;
     } catch (const Xapian::InvalidArgumentError) {
-       /* We'll let the philosopher's try to wrestle with the
+       /* 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. */
@@ -1193,6 +1263,41 @@ _notmuch_message_remove_term (notmuch_message_t *message,
     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
 }
 
+notmuch_private_status_t
+_notmuch_message_has_term (notmuch_message_t *message,
+                          const char *prefix_name,
+                          const char *value,
+                          notmuch_bool_t *result)
+{
+    char *term;
+    notmuch_bool_t out = FALSE;
+    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)
+       return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
+
+    try {
+       /* Look for the exact term */
+       Xapian::TermIterator i = message->doc.termlist_begin ();
+       i.skip_to (term);
+       if (i != message->doc.termlist_end () &&
+           !strcmp ((*i).c_str (), term))
+           out = TRUE;
+    } catch (Xapian::Error &error) {
+       status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
+    }
+    talloc_free (term);
+
+    *result = out;
+    return status;
+}
+
 notmuch_status_t
 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
 {
index 5dd4770e96190276c3e5077d977cb200aa6f6b82..92807975bb94562faefc949c19c080b9ad76618e 100644 (file)
@@ -279,6 +279,12 @@ _notmuch_message_remove_term (notmuch_message_t *message,
                              const char *prefix_name,
                              const char *value);
 
+notmuch_private_status_t
+_notmuch_message_has_term (notmuch_message_t *message,
+                          const char *prefix_name,
+                          const char *value,
+                          notmuch_bool_t *result);
+
 notmuch_private_status_t
 _notmuch_message_gen_terms (notmuch_message_t *message,
                            const char *prefix_name,
@@ -477,6 +483,17 @@ void
 _notmuch_doc_id_set_remove (notmuch_doc_id_set_t *doc_ids,
                             unsigned int doc_id);
 
+/* querying xapian documents by type (e.g. "mail" or "ghost"): */
+notmuch_status_t
+_notmuch_query_search_documents (notmuch_query_t *query,
+                                const char *type,
+                                notmuch_messages_t **out);
+
+notmuch_status_t
+_notmuch_query_count_documents (notmuch_query_t *query,
+                               const char *type,
+                               unsigned *count_out);
+
 /* message.cc */
 
 void
index 85b56bf1e4c34e96fe37e4df7a06eb4b762f2e75..cb46fc057d3e3ee91b7b9fcb5ae02667a2ab2c5f 100644 (file)
@@ -59,8 +59,17 @@ NOTMUCH_BEGIN_DECLS
 #define LIBNOTMUCH_MINOR_VERSION       3
 #define LIBNOTMUCH_MICRO_VERSION       0
 
+
+#if defined (__clang_major__) && __clang_major__ >= 3 \
+    || defined (__GNUC__) && __GNUC__ >= 5 \
+    || defined (__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ >= 5
 #define NOTMUCH_DEPRECATED(major,minor) \
     __attribute__ ((deprecated ("function deprecated as of libnotmuch " #major "." #minor)))
+#else
+#define NOTMUCH_DEPRECATED(major,minor) __attribute__ ((deprecated))
+#endif
+
+
 #endif /* __DOXYGEN__ */
 
 /**
@@ -1752,7 +1761,7 @@ notmuch_filenames_t *
 notmuch_directory_get_child_files (notmuch_directory_t *directory);
 
 /**
- * Get a notmuch_filenams_t iterator listing all the filenames of
+ * Get a notmuch_filenames_t iterator listing all the filenames of
  * sub-directories in the database within the given directory.
  *
  * The returned filenames will be the basename-entries only (not
index e627bfc2a3b5b576815172df2e4a2ae67e5c069b..77a7926ba80d6f13d1ca62ff7827bd18c9ef9c45 100644 (file)
@@ -186,6 +186,14 @@ notmuch_query_search_messages (notmuch_query_t *query)
 notmuch_status_t
 notmuch_query_search_messages_st (notmuch_query_t *query,
                                  notmuch_messages_t **out)
+{
+    return _notmuch_query_search_documents (query, "mail", out);
+}
+
+notmuch_status_t
+_notmuch_query_search_documents (notmuch_query_t *query,
+                                const char *type,
+                                notmuch_messages_t **out)
 {
     notmuch_database_t *notmuch = query->notmuch;
     const char *query_string = query->query_string;
@@ -208,7 +216,7 @@ notmuch_query_search_messages_st (notmuch_query_t *query,
        Xapian::Enquire enquire (*notmuch->xapian_db);
        Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
                                                   _find_prefix ("type"),
-                                                  "mail"));
+                                                  type));
        Xapian::Query string_query, final_query, exclude_query;
        Xapian::MSet mset;
        Xapian::MSetIterator iterator;
@@ -553,6 +561,12 @@ notmuch_query_count_messages (notmuch_query_t *query)
 
 notmuch_status_t
 notmuch_query_count_messages_st (notmuch_query_t *query, unsigned *count_out)
+{
+    return _notmuch_query_count_documents (query, "mail", count_out);
+}
+
+notmuch_status_t
+_notmuch_query_count_documents (notmuch_query_t *query, const char *type, unsigned *count_out)
 {
     notmuch_database_t *notmuch = query->notmuch;
     const char *query_string = query->query_string;
@@ -562,7 +576,7 @@ notmuch_query_count_messages_st (notmuch_query_t *query, unsigned *count_out)
        Xapian::Enquire enquire (*notmuch->xapian_db);
        Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
                                                   _find_prefix ("type"),
-                                                  "mail"));
+                                                  type));
        Xapian::Query string_query, final_query, exclude_query;
        Xapian::MSet mset;
        unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
index 3bd2903ec54aba42ec09a90e3ecc02dbd5648538..b3d0b668bc4d142726881c6eb8e471f181de3583 100644 (file)
@@ -31,6 +31,8 @@
 #include <gmime/gmime.h>
 
 typedef GMimeCryptoContext notmuch_crypto_context_t;
+/* This is automatically included only since gmime 2.6.10 */
+#include <gmime/gmime-pkcs7-context.h>
 
 #include "notmuch.h"
 
@@ -70,6 +72,7 @@ typedef struct notmuch_show_format {
 
 typedef struct notmuch_crypto {
     notmuch_crypto_context_t* gpgctx;
+    notmuch_crypto_context_t* pkcs7ctx;
     notmuch_bool_t verify;
     notmuch_bool_t decrypt;
     const char *gpgpath;
@@ -407,8 +410,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
  * verified. If crypto->decrypt is true, encrypted child parts will be
- * decrypted.  If crypto->gpgctx is NULL, it will be lazily
- * initialized.
+ * decrypted.  If the crypto contexts (crypto->gpgctx or
+ * crypto->pkcs7) are NULL, they will be lazily initialized.
  *
  * Return value:
  *
@@ -459,6 +462,11 @@ print_status_query (const char *loc,
                    const notmuch_query_t *query,
                    notmuch_status_t status);
 
+notmuch_status_t
+print_status_database (const char *loc,
+                      const notmuch_database_t *database,
+                      notmuch_status_t status);
+
 #include "command-line-arguments.h"
 
 extern char *notmuch_requested_db_uuid;
index 016fa12613c49c217b11fa8959c55a1e50a8e40f..4404cd7c33b870c9fa6c1a26c24143852b6e9c25 100755 (executable)
@@ -30,8 +30,8 @@ escape ()
     printf -v $2 '%s' "${__escape_arg__//\"/\\\"}"
 }
 
-EMACS=${EMACS-emacs}
-EMACSCLIENT=${EMACSCLIENT-emacsclient}
+EMACS=${EMACS:-emacs}
+EMACSCLIENT=${EMACSCLIENT:-emacsclient}
 
 PRINT_ONLY=
 NO_WINDOW=
index d45d0af8642a3f2f4815808c91509e5a0145eda6..04cb5cac092a9cf08def0af2645f89a5d0394c5f 100644 (file)
@@ -281,6 +281,10 @@ add_file (notmuch_database_t *notmuch, const char *filename,
        fprintf (stderr, "Note: Ignoring non-mail file: %s\n", filename);
        break;
     /* Fatal issues. Don't process anymore. */
+    case NOTMUCH_STATUS_FILE_ERROR:
+       fprintf (stderr, "Unexpected error with file %s\n", filename);
+       (void) print_status_database ("add_file", notmuch, status);
+       goto DONE;
     case NOTMUCH_STATUS_READ_ONLY_DATABASE:
     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
     case NOTMUCH_STATUS_OUT_OF_MEMORY:
@@ -445,7 +449,7 @@ add_files (notmuch_database_t *notmuch,
         */
        if (_entry_in_ignore_list (entry->d_name, state)) {
            if (state->debug)
-               printf ("(D) add_files_recursive, pass 1: explicitly ignoring %s/%s\n",
+               printf ("(D) add_files, pass 1: explicitly ignoring %s/%s\n",
                        path, entry->d_name);
            continue;
        }
@@ -513,9 +517,8 @@ add_files (notmuch_database_t *notmuch,
        /* Ignore files & directories user has configured to be ignored */
        if (_entry_in_ignore_list (entry->d_name, state)) {
            if (state->debug)
-               printf ("(D) add_files_recursive, pass 2: explicitly ignoring %s/%s\n",
-                       path,
-                       entry->d_name);
+               printf ("(D) add_files, pass 2: explicitly ignoring %s/%s\n",
+                       path, entry->d_name);
            continue;
        }
 
@@ -529,7 +532,7 @@ add_files (notmuch_database_t *notmuch,
                                              notmuch_filenames_get (db_files));
 
            if (state->debug)
-               printf ("(D) add_files_recursive, pass 2: queuing passed file %s for deletion from database\n",
+               printf ("(D) add_files, pass 2: queuing passed file %s for deletion from database\n",
                        absolute);
 
            _filename_list_add (state->removed_files, absolute);
@@ -547,7 +550,7 @@ add_files (notmuch_database_t *notmuch,
                char *absolute = talloc_asprintf (state->removed_directories,
                                                  "%s/%s", path, filename);
                if (state->debug)
-                   printf ("(D) add_files_recursive, pass 2: queuing passed directory %s for deletion from database\n",
+                   printf ("(D) add_files, pass 2: queuing passed directory %s for deletion from database\n",
                        absolute);
 
                _filename_list_add (state->removed_directories, absolute);
@@ -618,7 +621,7 @@ add_files (notmuch_database_t *notmuch,
                                          "%s/%s", path,
                                          notmuch_filenames_get (db_files));
        if (state->debug)
-           printf ("(D) add_files_recursive, pass 3: queuing leftover file %s for deletion from database\n",
+           printf ("(D) add_files, pass 3: queuing leftover file %s for deletion from database\n",
                    absolute);
 
        _filename_list_add (state->removed_files, absolute);
@@ -633,7 +636,7 @@ add_files (notmuch_database_t *notmuch,
                                          notmuch_filenames_get (db_subdirs));
 
        if (state->debug)
-           printf ("(D) add_files_recursive, pass 3: queuing leftover directory %s for deletion from database\n",
+           printf ("(D) add_files, pass 3: queuing leftover directory %s for deletion from database\n",
                    absolute);
 
        _filename_list_add (state->removed_directories, absolute);
index 1357142903d5771566678a132f15c6537e4e7714..3c6d685cbd6054c68a0281e696e747c8045aff57 100644 (file)
@@ -80,7 +80,8 @@ format_part_reply (mime_node_t *node)
            show_text_part_content (node->part, stream_stdout, NOTMUCH_SHOW_TEXT_PART_REPLY);
            g_object_unref(stream_stdout);
        } else if (disposition &&
-                  strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) {
+                  strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                              GMIME_DISPOSITION_ATTACHMENT) == 0) {
            const char *filename = g_mime_part_get_filename (GMIME_PART (node->part));
            printf ("Attachment: %s (%s)\n", filename,
                    g_mime_content_type_to_string (content_type));
@@ -331,7 +332,7 @@ add_recipients_from_message (GMimeMessage *reply,
      * field and use the From header. This ensures the original sender
      * will get the reply even if not subscribed to the list. Note
      * that the address in the Reply-To header will always appear in
-     * the reply.
+     * the reply if reply_all is true.
      */
     if (reply_to_header_is_redundant (message)) {
        reply_to_map[0].header = "from";
index 5a83c6056ca5de85ef52b5dbb64359904dbba1a4..87e52bbc0e81a7f8b0c4c7ca5c374cfdbb5da068 100644 (file)
@@ -456,7 +456,8 @@ format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
            g_mime_part_get_filename (GMIME_PART (node->part)) : NULL;
 
        if (disposition &&
-           strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+           strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                       GMIME_DISPOSITION_ATTACHMENT) == 0)
            part_type = "attachment";
        else
            part_type = "part";
index c020cb6f40b8ad3677aac9fa6104ef3d4aba78b6..0d1532826a2e81999ba833d4b18fa800fc060d81 100644 (file)
@@ -237,10 +237,6 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[])
            fprintf (stderr, "Can't specify both cmdline and stdin!\n");
            return EXIT_FAILURE;
        }
-       if (remove_all) {
-           fprintf (stderr, "Can't specify both --remove-all and --batch\n");
-           return EXIT_FAILURE;
-       }
     } else {
        tag_ops = tag_op_list_create (config);
        if (tag_ops == NULL) {
index 8fa81cbf7a2b85c2bf671c517b9edc212622d8b4..45d3fb4e230e9426a194e045421aaaa6dcba684b 100644 (file)
--- a/status.c
+++ b/status.c
@@ -19,3 +19,20 @@ print_status_query (const char *loc,
     }
     return status;
 }
+
+notmuch_status_t
+print_status_database (const char *loc,
+                   const notmuch_database_t *notmuch,
+                   notmuch_status_t status)
+{
+    if (status) {
+       const char *msg;
+
+       fprintf (stderr, "%s: %s\n", loc,
+                notmuch_status_to_string (status));
+       msg = notmuch_database_status_string (notmuch);
+       if (msg)
+           fputs (msg, stderr);
+    }
+    return status;
+}
index 0f7d5bfcbf3278d7d7dfe051f39a777fe8f018fb..0579feef25c659f6da7752b54e93bb672d95b21c 100644 (file)
@@ -7,4 +7,5 @@ smtp-dummy
 symbol-test
 make-db-version
 test-results
+ghost-report
 tmp.*
index 2b18691403aaa101ab81878c22f957eb1a1f034b..022f2cf7af0bfa3b9bec1b64505c49a651434db5 100644 (file)
@@ -38,6 +38,9 @@ $(dir)/parse-time: $(dir)/parse-time.o parse-time-string/parse-time-string.o
 $(dir)/make-db-version: $(dir)/make-db-version.o
        $(call quiet,CXX) $^ -o $@ $(LDFLAGS) $(XAPIAN_LDFLAGS)
 
+$(dir)/ghost-report: $(dir)/ghost-report.o
+       $(call quiet,CXX) $^ -o $@ $(LDFLAGS) $(XAPIAN_LDFLAGS)
+
 .PHONY: test check
 
 test_main_srcs=$(dir)/arg-test.c \
@@ -47,6 +50,7 @@ test_main_srcs=$(dir)/arg-test.c \
              $(dir)/smtp-dummy.c \
              $(dir)/symbol-test.cc \
              $(dir)/make-db-version.cc \
+             $(dir)/ghost-report.cc
 
 test_srcs=$(test_main_srcs) $(dir)/database-test.c
 
@@ -57,7 +61,7 @@ test-binaries: $(TEST_BINARIES)
 
 test:  all test-binaries
 ifeq ($V,)
-       @echo 'Use "$(MAKE) V=1" to print test headings and PASSIng results.'
+       @echo 'Use "$(MAKE) V=1" to print test headings and PASSing results.'
        @env NOTMUCH_TEST_QUIET=1 ${test_src_dir}/notmuch-test $(OPTIONS)
 else
 # The user has explicitly enabled quiet execution.
index e54e36b795e852e536dab96256420edbfee84368..bd9ab5470c7c499c39d41fa858faebe733e180b7 100644 (file)
@@ -8,10 +8,17 @@ enhance.
 
 Prerequisites
 -------------
+The test system itself requires:
+
+  - bash(1) version 4.0 or newer
+
+Without bash 4.0+ the tests just refuse to run.
+
 Some tests require external dependencies to run. Without them, they
 will be skipped, or (rarely) marked failed. Please install these, so
 that you know if you break anything.
 
+  - GNU tar(1)
   - dtach(1)
   - emacs(1)
   - emacsclient(1)
@@ -19,14 +26,21 @@ that you know if you break anything.
   - gpg(1)
   - python(1)
 
+If your system lacks these tools or have older, non-upgreable versions
+of these, please (possibly compile and) install these to some other
+path, for example /usr/local/bin or /opt/gnu/bin. Then prepend the
+chosen directory to your PATH before running the tests.
+
+e.g. env PATH=/opt/gnu/bin:$PATH make test
+
 Running Tests
 -------------
 The easiest way to run tests is to say "make test", (or simply run the
 notmuch-test script). Either command will run all available tests.
 
 Alternately, you can run a specific subset of tests by simply invoking
-one of the executable scripts in this directory, (such as ./search,
-./reply, etc). Note that you will probably want "make test-binaries"
+one of the executable scripts in this directory, (such as ./T*-search.sh,
+./T*-reply.sh, etc). Note that you will probably want "make test-binaries"
 before running individual tests.
 
 The following command-line options are available when running tests:
@@ -80,9 +94,9 @@ can be specified as follows:
 You can choose an emacs binary (and corresponding emacsclient) to run
 the tests in one of the following ways.
 
-       TEST_EMACS=my-special-emacs TEST_EMACSCLIENT=my-emacsclient make test
-       TEST_EMACS=my-special-emacs TEST_EMACSCLIENT=my-emacsclient ./emacs
-       make test TEST_EMACS=my-special-emacs TEST_EMACSCLIENT=my-emacsclient
+       TEST_EMACS=my-emacs TEST_EMACSCLIENT=my-emacsclient make test
+       TEST_EMACS=my-emacs TEST_EMACSCLIENT=my-emacsclient ./T*-emacs.sh
+       make test TEST_EMACS=my-emacs TEST_EMACSCLIENT=my-emacsclient
 
 Some tests may require a c compiler. You can choose the name and flags similarly
 to with emacs, e.g.
@@ -126,9 +140,13 @@ skipped by the user, as failures.
 
 Writing Tests
 -------------
-The test script is written as a shell script.  It should start with
-the standard "#!/usr/bin/env bash" with copyright notices, and an
-assignment to variable 'test_description', like this:
+The test script is written as a shell script. It is to be named as
+Tddd-testname.sh where 'ddd' is three digits and 'testname' the "bare"
+name of your test. Tests will be run in order the 'ddd' part determines.
+
+The test script should start with the standard "#!/usr/bin/env bash"
+with copyright notices, and an assignment to variable 'test_description',
+like this:
 
        #!/usr/bin/env bash
        #
index 81cf2fad10ad27bcf793ebe2c1a0a126dce586ba..beeb574a3b30e36fb6e7694b385a769701f0e54e 100755 (executable)
@@ -64,7 +64,7 @@ generate_message
 notmuch new > /dev/null
 mv "$gen_msg_filename" "${gen_msg_filename}"-renamed
 output=$(NOTMUCH_NEW --debug)
-test_expect_equal "$output" "(D) add_files_recursive, pass 2: queuing passed file ${gen_msg_filename} for deletion from database
+test_expect_equal "$output" "(D) add_files, pass 2: queuing passed file ${gen_msg_filename} for deletion from database
 No new mail. Detected 1 file rename."
 
 
@@ -72,7 +72,7 @@ test_begin_subtest "Deleted message"
 
 rm "${gen_msg_filename}"-renamed
 output=$(NOTMUCH_NEW --debug)
-test_expect_equal "$output" "(D) add_files_recursive, pass 3: queuing leftover file ${gen_msg_filename}-renamed for deletion from database
+test_expect_equal "$output" "(D) add_files, pass 3: queuing leftover file ${gen_msg_filename}-renamed for deletion from database
 No new mail. Removed 1 message."
 
 
@@ -88,7 +88,7 @@ notmuch new > /dev/null
 mv "${MAIL_DIR}"/dir "${MAIL_DIR}"/dir-renamed
 
 output=$(NOTMUCH_NEW --debug)
-test_expect_equal "$output" "(D) add_files_recursive, pass 2: queuing passed directory ${MAIL_DIR}/dir for deletion from database
+test_expect_equal "$output" "(D) add_files, pass 2: queuing passed directory ${MAIL_DIR}/dir for deletion from database
 No new mail. Detected 3 file renames."
 
 
@@ -96,7 +96,7 @@ test_begin_subtest "Deleted directory"
 rm -rf "${MAIL_DIR}"/dir-renamed
 
 output=$(NOTMUCH_NEW --debug)
-test_expect_equal "$output" "(D) add_files_recursive, pass 2: queuing passed directory ${MAIL_DIR}/dir-renamed for deletion from database
+test_expect_equal "$output" "(D) add_files, pass 2: queuing passed directory ${MAIL_DIR}/dir-renamed for deletion from database
 No new mail. Removed 3 messages."
 
 
@@ -115,7 +115,7 @@ test_begin_subtest "Deleted directory (end of list)"
 rm -rf "${MAIL_DIR}"/zzz
 
 output=$(NOTMUCH_NEW --debug)
-test_expect_equal "$output" "(D) add_files_recursive, pass 3: queuing leftover directory ${MAIL_DIR}/zzz for deletion from database
+test_expect_equal "$output" "(D) add_files, pass 3: queuing leftover directory ${MAIL_DIR}/zzz for deletion from database
 No new mail. Removed 3 messages."
 
 
@@ -166,9 +166,18 @@ test_begin_subtest "Deleted two-level directory"
 rm -rf "${MAIL_DIR}"/two
 
 output=$(NOTMUCH_NEW --debug)
-test_expect_equal "$output" "(D) add_files_recursive, pass 3: queuing leftover directory ${MAIL_DIR}/two for deletion from database
+test_expect_equal "$output" "(D) add_files, pass 3: queuing leftover directory ${MAIL_DIR}/two for deletion from database
 No new mail. Removed 3 messages."
 
+test_begin_subtest "One character directory at top level"
+
+generate_message [dir]=A
+generate_message [dir]=A/B
+generate_message [dir]=A/B/C
+
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 3 new messages to the database."
+
 test_begin_subtest "Support single-message mbox"
 cat > "${MAIL_DIR}"/mbox_file1 <<EOF
 From test_suite@notmuchmail.org Fri Jan  5 15:43:57 2001
@@ -227,20 +236,20 @@ mkdir -p "${MAIL_DIR}"/one/two/three/.git
 touch "${MAIL_DIR}"/{one,one/two,one/two/three}/ignored_file
 output=$(NOTMUCH_NEW --debug 2>&1 | sort)
 test_expect_equal "$output" \
-"(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/.git
-(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
-(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/ignored_file
-(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/ignored_file
-(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
-(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
-(D) add_files_recursive, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
-(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/.git
-(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
-(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/ignored_file
-(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/ignored_file
-(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
-(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
-(D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+"(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
 No new mail."
 
 
@@ -284,9 +293,9 @@ notmuch config set new.tags $OLDCONFIG
 
 
 test_begin_subtest "Xapian exception: read only files"
-chmod u-w  ${MAIL_DIR}/.notmuch/xapian/*.DB
+chmod u-w  ${MAIL_DIR}/.notmuch/xapian/*.${db_ending}
 output=$(NOTMUCH_NEW --debug 2>&1 | sed 's/: .*$//' )
-chmod u+w  ${MAIL_DIR}/.notmuch/xapian/*.DB
+chmod u+w  ${MAIL_DIR}/.notmuch/xapian/*.${db_ending}
 test_expect_equal "$output" "A Xapian exception occurred opening database"
 
 test_done
index 3fec94e899cc7925563405b724fb79a5620695bb..0ac8314087cd9c985f97b79170a50d37d5657c72 100755 (executable)
@@ -95,7 +95,7 @@ test_expect_equal_file EXPECTED OUTPUT
 
 backup_database
 test_begin_subtest "error message for database open"
-dd if=/dev/zero of="${MAIL_DIR}/.notmuch/xapian/postlist.DB" count=3
+dd if=/dev/zero of="${MAIL_DIR}/.notmuch/xapian/postlist.${db_ending}" count=3
 notmuch count '*' 2>OUTPUT 1>/dev/null
 output=$(sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' OUTPUT)
 test_expect_equal "${output}" "A Xapian exception occurred opening database"
@@ -105,7 +105,7 @@ cat <<EOF > count-files.gdb
 set breakpoint pending on
 break count_files
 commands
-shell cp /dev/null ${MAIL_DIR}/.notmuch/xapian/postlist.DB
+shell cp /dev/null ${MAIL_DIR}/.notmuch/xapian/postlist.${db_ending}
 continue
 end
 run
index 821d39334e5c9684551c7dc4f03afa7502618b81..a451ffaeb39e3db7529332564b214c8d00f90b8a 100755 (executable)
@@ -38,6 +38,17 @@ test_expect_equal "$output" "\
 thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One ()
 thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (tag5 tag6 unread)"
 
+test_begin_subtest "Remove all with batch"
+notmuch tag +tag1 One
+notmuch tag --remove-all --batch <<EOF
+-- One
++tag3 +tag4 +inbox -- Two
+EOF
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One ()
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag3 tag4)"
+
 test_begin_subtest "Remove all with a no-op"
 notmuch tag +inbox +tag1 +unread One
 notmuch tag --remove-all +foo +inbox +tag1 -foo +unread Two
@@ -102,6 +113,20 @@ notmuch search \* | notmuch_search_sanitize > OUTPUT
 notmuch restore --format=batch-tag < backup.tags
 test_expect_equal_file batch.expected OUTPUT
 
+test_begin_subtest "--batch --input --remove-all"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag +foo +bar -- One
+notmuch tag +tag7 -- Two
+notmuch tag --batch --input=batch.in --remove-all
+notmuch search \* | notmuch_search_sanitize > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+cat > batch_removeall.expected <<EOF
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (@ tag6)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (tag6)
+EOF
+test_expect_equal_file batch_removeall.expected OUTPUT
+rm batch_removeall.expected
+
 test_begin_subtest "--batch, blank lines and comments"
 notmuch dump | sort > EXPECTED
 notmuch tag --batch <<EOF
@@ -262,9 +287,9 @@ test_expect_code 1 "Empty tag names" 'notmuch tag + One'
 test_expect_code 1 "Tag name beginning with -" 'notmuch tag +- One'
 
 test_begin_subtest "Xapian exception: read only files"
-chmod u-w  ${MAIL_DIR}/.notmuch/xapian/*.DB
+chmod u-w  ${MAIL_DIR}/.notmuch/xapian/*.${db_ending}
 output=$(notmuch tag +something '*' 2>&1 | sed 's/: .*$//' )
-chmod u+w  ${MAIL_DIR}/.notmuch/xapian/*.DB
+chmod u+w  ${MAIL_DIR}/.notmuch/xapian/*.${db_ending}
 test_expect_equal "$output" "A Xapian exception occurred opening database"
 
 test_done
index 7c4c9f71913fc695a29a668294d5ece21cf6f0d5..35678909b0ce110ca650c108a9478aae4139e5dd 100755 (executable)
@@ -763,4 +763,56 @@ test_begin_subtest "indexes mime-type #3"
 output=$(notmuch search from:todd and mimetype:multipart/alternative | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2014-01-12 [1/1] Todd; odd content types (inbox unread)"
 
+test_begin_subtest "case of Content-Disposition doesn't matter for indexing"
+cat <<EOF > ${MAIL_DIR}/content-disposition
+Return-path: <david@tethera.net>
+Envelope-to: david@tethera.net
+Delivery-date: Sun, 04 Oct 2015 09:16:03 -0300
+Received: from gitolite.debian.net ([87.98.215.224])
+       by yantan.tethera.net with esmtps (TLS1.2:DHE_RSA_AES_128_CBC_SHA1:128)
+       (Exim 4.80)
+       (envelope-from <david@tethera.net>)
+       id 1ZiiCx-0007iz-RK
+       for david@tethera.net; Sun, 04 Oct 2015 09:16:03 -0300
+Received: from remotemail by gitolite.debian.net with local (Exim 4.80)
+       (envelope-from <david@tethera.net>)
+       id 1ZiiC8-0002Rz-Uf; Sun, 04 Oct 2015 12:15:12 +0000
+Received: (nullmailer pid 28621 invoked by uid 1000); Sun, 04 Oct 2015
+ 12:14:53 -0000
+From: David Bremner <david@tethera.net>
+To: David Bremner <david@tethera.net>
+Subject: test attachment
+User-Agent: Notmuch/0.20.2+93~g33c8777 (http://notmuchmail.org) Emacs/24.5.1
+ (x86_64-pc-linux-gnu)
+Date: Sun, 04 Oct 2015 09:14:53 -0300
+Message-ID: <87io6m96f6.fsf@zancas.localnet>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+Content-Disposition: ATTACHMENT; filename=hello.txt
+Content-Description: this is a very exciting file
+
+hello
+
+--=-=-=
+Content-Type: text/plain
+
+
+world
+
+--=-=-=--
+
+EOF
+NOTMUCH_NEW
+
+cat <<EOF > EXPECTED
+attachment
+inbox
+unread
+EOF
+
+notmuch search --output=tags id:87io6m96f6.fsf@zancas.localnet > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
 test_done
index 61bc369af494208aa919073f244d835a3912ca45..daa02568f5f988f6638753fd20887b4b66a9e8ba 100755 (executable)
@@ -130,7 +130,7 @@ test_expect_equal "$output" "thread:XXX   2009-11-18 [4/4] Jjgod Jiang, Alexande
 test_begin_subtest "Add tag (large query)"
 # We use a long query to force us into batch mode and use a funny tag
 # that requires escaping for batch tagging.
-test_emacs "(notmuch-tag (concat \"$os_x_darwin_thread\" \" or \" (make-string notmuch-tag-argument-limit ?x)) (list \"+tag-from-%-large-query\"))"
+test_emacs "(notmuch-tag (concat \"$os_x_darwin_thread\" \" or \" (mapconcat #'identity (make-list notmuch-tag-argument-limit \"x\") \"-\")) (list \"+tag-from-%-large-query\"))"
 output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-%-large-query unread)"
 notmuch tag -tag-from-%-large-query $os_x_darwin_thread
@@ -394,6 +394,8 @@ User-Agent: Notmuch/XXX Emacs/XXX
 --text follows this line--
 Adrian Perez de Castro <aperez@igalia.com> writes:
 
+> [ Unknown signature status ]
+>
 > Hello to all,
 >
 > I have just heard about Not Much today in some random Linux-related news
@@ -473,6 +475,38 @@ Alex Botero-Lowry <alex.boterolowry@gmail.com> writes:
 > and http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.htmlspecifically
 > uses 64 as the
 > buffer size.
+> From e3bc4bbd7b9d0d086816ab5f8f2d6ffea1dd3ea4 Mon Sep 17 00:00:00 2001
+> From: Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+> Date: Tue, 17 Nov 2009 11:30:39 -0800
+> Subject: [PATCH] Deal with situation where sysconf(_SC_GETPW_R_SIZE_MAX) returns -1
+>
+> ---
+>  notmuch-config.c |    2 ++
+>  1 files changed, 2 insertions(+), 0 deletions(-)
+>
+> diff --git a/notmuch-config.c b/notmuch-config.c
+> index 248149c..e7220d8 100644
+> --- a/notmuch-config.c
+> +++ b/notmuch-config.c
+> @@ -77,6 +77,7 @@ static char *
+>  get_name_from_passwd_file (void *ctx)
+>  {
+>      long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
+> +    if (pw_buf_size == -1) pw_buf_size = 64;
+>      char *pw_buf = talloc_zero_size (ctx, pw_buf_size);
+>      struct passwd passwd, *ignored;
+>      char *name;
+> @@ -101,6 +102,7 @@ static char *
+>  get_username_from_passwd_file (void *ctx)
+>  {
+>      long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
+> +    if (pw_buf_size == -1) pw_buf_size = 64;
+>      char *pw_buf = talloc_zero_size (ctx, pw_buf_size);
+>      struct passwd passwd, *ignored;
+>      char *name;
+> -- 
+> 1.6.5.2
+>
 > _______________________________________________
 > notmuch mailing list
 > notmuch@notmuchmail.org
diff --git a/test/T355-smime.sh b/test/T355-smime.sh
new file mode 100755 (executable)
index 0000000..d942412
--- /dev/null
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+
+test_description='S/MIME signature verification and decryption'
+. ./test-lib.sh || exit 1
+
+add_gpgsm_home ()
+{
+    local fpr
+    [ -d ${GNUPGHOME} ] && return
+    mkdir -m 0700 "$GNUPGHOME"
+    gpgsm --no-tty --no-common-certs-import --disable-dirmngr --import < $TEST_DIRECTORY/smime/test.crt >"$GNUPGHOME"/import.log 2>&1
+    fpr=$(gpgsm  --list-key test_suite@notmuchmail.org | sed -n 's/.*fingerprint: //p')
+    echo "$fpr S relax" >> $GNUPGHOME/trustlist.txt
+    test_debug "cat $GNUPGHOME/import.log"
+}
+
+test_require_external_prereq openssl
+test_require_external_prereq gpgsm
+
+cp $TEST_DIRECTORY/smime/key+cert.pem test_suite.pem
+
+FINGERPRINT=$(openssl x509 -fingerprint -in test_suite.pem -noout | sed -e 's/^.*=//' -e s/://g)
+
+add_gpgsm_home
+
+test_expect_success 'emacs delivery of S/MIME signed message' \
+     'emacs_fcc_message \
+     "test signed message 001" \
+     "This is a test signed message." \
+     "(mml-secure-message-sign \"smime\")"'
+
+# Hard code the MML to avoid several interactive questions
+test_expect_success 'emacs delivery of S/MIME encrypted + signed message' \
+'emacs_fcc_message \
+    "test encrypted message 001" \
+    "<#secure method=smime mode=signencrypt keyfile=\\\"test_suite.pem\\\" certfile=\\\"test_suite.pem\\\">\nThis is a test encrypted message.\n"'
+
+test_begin_subtest "Signature verification (openssl)"
+notmuch show --format=raw subject:"test signed message 001" |\
+    openssl smime -verify -CAfile $TEST_DIRECTORY/smime/test.crt 2>OUTPUT
+cat <<EOF > EXPECTED
+Verification successful
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "signature verification (notmuch CLI)"
+output=$(notmuch show --format=json --verify subject:"test signed message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [-1234567890]*|"created": 946728000|' \
+         -e 's|"expires": [-1234567890]*|"expires": 424242424|' )
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": "YYYYY",
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "headers": {"Subject": "test signed message 001",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "test_suite@notmuchmail.org",
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
+ "body": [{"id": 1,
+ "sigstatus": [{"status": "good",
+ "fingerprint": "'$FINGERPRINT'",
+ "expires": 424242424,
+ "created": 946728000}],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "This is a test signed message.\n"},
+ {"id": 3,
+  "content-length": 1922,
+  "content-transfer-encoding": "base64",
+  "content-type": "application/x-pkcs7-signature",
+  "filename": "smime.p7s"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "Decryption and signature verification (openssl)"
+notmuch show --format=raw subject:"test encrypted message 001" |\
+    openssl smime -decrypt -recip test_suite.pem |\
+    openssl smime -verify -CAfile $TEST_DIRECTORY/smime/test.crt 2>OUTPUT
+cat <<EOF > EXPECTED
+Verification successful
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
index 4ec0ea65710baba535cc7910fd3e55895f7edce6..3f18ec1a34fc631f9e7d15b49fffe83cc3f741b6 100755 (executable)
@@ -15,11 +15,11 @@ test_begin_subtest 'running test' run_test
 mkdir -p ${PWD}/fakedb/.notmuch
 ( LD_LIBRARY_PATH="$TEST_DIRECTORY/../lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
                 $TEST_DIRECTORY/symbol-test ${PWD}/fakedb ${PWD}/nonexistent \
-                2>&1 | sed "s,${PWD},CWD,g") > OUTPUT
+                2>&1 | notmuch_dir_sanitize | sed -e "s,\`,\',g" -e "s,${NOTMUCH_DEFAULT_XAPIAN_BACKEND},backend,g") > OUTPUT
 
 cat <<EOF > EXPECTED
 A Xapian exception occurred opening database: Couldn't stat 'CWD/fakedb/.notmuch/xapian'
-caught No chert database found at path \`CWD/nonexistent'
+caught No backend database found at path 'CWD/nonexistent'
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
index 7faf03d335099d92c6a4d4942441904e019ad4b0..a3a7d39c5251acd32edc987041596bb4601e40db 100755 (executable)
@@ -116,25 +116,4 @@ MAIL_DIR/bar/new/21:2,
 MAIL_DIR/bar/new/22:2,
 MAIL_DIR/cur/51:2,"
 
-# Ghost messages are difficult to test since they're nearly invisible.
-# However, if the upgrade works correctly, the ghost message should
-# retain the right thread ID even if all of the original messages in
-# the thread are deleted.  That's what we test.  This won't detect if
-# the upgrade just plain didn't happen, but it should detect if
-# something went wrong.
-test_begin_subtest "ghost message retains thread ID"
-# Upgrade database
-notmuch new
-# Get thread ID of real message
-thread=$(notmuch search --output=threads id:4EFC743A.3060609@april.org)
-# Delete all real messages in that thread
-rm $(notmuch search --output=files $thread)
-notmuch new
-# "Deliver" ghost message
-add_message '[subject]=Ghost' '[id]=4EFC3931.6030007@april.org'
-# If the ghost upgrade worked, the new message should be attached to
-# the existing thread ID.
-nthread=$(notmuch search --output=threads id:4EFC3931.6030007@april.org)
-test_expect_equal "$thread" "$nthread"
-
 test_done
index c280939c54fab4cc83e5aa29a09dbaf0342bdc41..087c6bd74707736384994f71d8720e2539475233 100755 (executable)
@@ -183,13 +183,13 @@ int main (int argc, char** argv)
 EOF
 cat <<'EOF' >EXPECTED
 == stdout ==
-Path already exists: CWD/mail
+Path already exists: MAIL_DIR
 
 == stderr ==
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
-cat <<'EOF' > c_head
+cat <<EOF > c_head
 #include <stdio.h>
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -202,16 +202,20 @@ 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 (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db);
+   stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg);
    if (stat != NOTMUCH_STATUS_SUCCESS) {
-     fprintf (stderr, "error opening database: %d\n", stat);
+     fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
+     exit (1);
    }
-   path = talloc_asprintf (db, "%s/.notmuch/xapian/postlist.DB", argv[1]);
+   path = talloc_asprintf (db, "%s/.notmuch/xapian/postlist.${db_ending}", argv[1]);
    fd = open(path,O_WRONLY|O_TRUNC);
-   if (fd < 0)
-       fprintf (stderr, "error opening %s\n");
+   if (fd < 0) {
+       fprintf (stderr, "error opening %s\n", argv[1]);
+       exit (1);
+   }
 EOF
 cat <<'EOF' > c_tail
    if (stat) {
diff --git a/test/T580-thread-search.sh b/test/T580-thread-search.sh
new file mode 100755 (executable)
index 0000000..6f7106d
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2015 David Bremner
+#
+
+test_description='test of searching by thread-id'
+
+. ./test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "Every message is found in exactly one thread"
+
+count=0
+success=0
+for id in $(notmuch search --output=messages '*'); do
+    count=$((count +1))
+    matches=$(notmuch search --output=threads "$id" | wc -l)
+    if [ "$matches" = 1 ]; then
+       success=$((success + 1))
+    fi
+done
+
+test_expect_equal "$count" "$success"
+
+test_begin_subtest "roundtripping message-ids via thread-ids"
+
+count=0
+success=0
+for id in $(notmuch search --output=messages '*'); do
+    count=$((count +1))
+    thread=$(notmuch search --output=threads "$id")
+    matched=$(notmuch search --output=messages "$thread" | grep "$id")
+    if [ "$matched" = "$id" ]; then
+       success=$((success + 1))
+    fi
+done
+
+test_expect_equal "$count" "$success"
+
+
+test_done
diff --git a/test/T590-thread-breakage.sh b/test/T590-thread-breakage.sh
new file mode 100755 (executable)
index 0000000..6e4031a
--- /dev/null
@@ -0,0 +1,124 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2016 Daniel Kahn Gillmor
+#
+
+test_description='thread breakage during reindexing
+
+notmuch uses ghost documents to track messages we have seen references
+to but have never seen.  Regardless of the order of delivery, message
+deletion, and reindexing, the list of ghost messages for a given
+stored corpus should not vary, so that threads can be reassmebled
+cleanly.
+
+In practice, we accept a small amount of variation (and therefore
+traffic pattern metadata leakage to be stored in the index) for the
+sake of efficiency.
+
+This test also embeds some subtests to ensure that indexing actually
+works properly and attempted fixes to threading issues do not break
+the expected contents of the index.'
+
+. ./test-lib.sh || exit 1
+
+message_a() {
+    mkdir -p ${MAIL_DIR}/cur
+    cat > ${MAIL_DIR}/cur/a <<EOF
+Subject: First message
+Message-ID: <a@example.net>
+From: Alice <alice@example.net>
+To: Bob <bob@example.net>
+Date: Thu, 31 Mar 2016 20:10:00 -0400
+
+This is the first message in the thread.
+Apple
+EOF
+}
+
+message_b() {
+    mkdir -p ${MAIL_DIR}/cur
+    cat > ${MAIL_DIR}/cur/b <<EOF
+Subject: Second message
+Message-ID: <b@example.net>
+In-Reply-To: <a@example.net>
+References: <a@example.net>
+From: Bob <bob@example.net>
+To: Alice <alice@example.net>
+Date: Thu, 31 Mar 2016 20:15:00 -0400
+
+This is the second message in the thread.
+Banana
+EOF
+}
+
+
+test_content_count() {
+    test_begin_subtest "${3:-looking for $2 instance of '$1'}"
+    count=$(notmuch count --output=threads "$1")
+    test_expect_equal "$count" "$2"
+}
+
+test_thread_count() {
+    test_begin_subtest "${2:-Expecting $1 thread(s)}"
+    count=$(notmuch count --output=threads)
+    test_expect_equal "$count" "$1"
+}
+
+test_ghost_count() {
+    test_begin_subtest "${2:-Expecting $1 ghosts(s)}"
+    ghosts=$(../ghost-report ${MAIL_DIR}/.notmuch/xapian)
+    test_expect_equal "$ghosts" "$1"
+}
+
+notmuch new >/dev/null
+
+test_thread_count 0 'There should be no threads initially'
+test_ghost_count 0 'There should be no ghosts initially'
+
+message_a
+notmuch new >/dev/null
+test_thread_count 1 'One message in: one thread'
+test_content_count apple 1
+test_content_count banana 0
+test_ghost_count 0
+
+message_b
+notmuch new >/dev/null
+test_thread_count 1 'Second message in the same thread: one thread'
+test_content_count apple 1
+test_content_count banana 1
+test_ghost_count 0
+
+rm -f ${MAIL_DIR}/cur/a
+notmuch new >/dev/null
+test_thread_count 1 'First message removed: still only one thread'
+test_content_count apple 0
+test_content_count banana 1
+test_ghost_count 1 'should be one ghost after first message removed'
+
+message_a
+notmuch new >/dev/null
+test_thread_count 1 'First message reappears: should return to the same thread'
+test_content_count apple 1
+test_content_count banana 1
+test_ghost_count 0
+
+rm -f ${MAIL_DIR}/cur/b
+notmuch new >/dev/null
+test_thread_count 1 'Removing second message: still only one thread'
+test_content_count apple 1
+test_content_count banana 0
+test_begin_subtest 'No ghosts should remain after deletion of second message'
+# this is known to fail; we are leaking ghost messages deliberately
+test_subtest_known_broken
+ghosts=$(../ghost-report ${MAIL_DIR}/.notmuch/xapian)
+test_expect_equal "$ghosts" "0"
+
+rm -f ${MAIL_DIR}/cur/a
+notmuch new >/dev/null
+test_thread_count 0 'All messages gone: no threads'
+test_content_count apple 0
+test_content_count banana 0
+test_ghost_count 0 'No ghosts should remain after full thread deletion'
+
+test_done
index 01a420512af11ea4b57345ec27a3cbc1a7e1ac6c..1ca52b9c4f73aad5966e44e3dff8505b1d9e348f 100644 (file)
@@ -29,16 +29,19 @@ class RenameBreakpoint(gdb.Breakpoint):
         self.n = 0
 
     def stop(self):
-        # As an optimization, only consider snapshots after a Xapian
-        # has really committed.  Xapian overwrites record.base? as the
-        # last step in the commit, so keep an eye on their inumbers.
-        inodes = {}
-        for path in glob.glob('%s/.notmuch/xapian/record.base*' % maildir):
-            inodes[path] = os.stat(path).st_ino
-        if inodes == self.last_inodes:
-            # Continue
-            return False
-        self.last_inodes = inodes
+        xapiandir = '%s/.notmuch/xapian' % maildir
+        if os.path.isfile('%s/iamchert' % xapiandir):
+            # As an optimization, only consider snapshots after a
+            # Xapian has really committed.  The chert backend
+            # overwrites record.base? as the last step in the commit,
+            # so keep an eye on their inumbers.
+            inodes = {}
+            for path in glob.glob('%s/record.base*' % xapiandir):
+                inodes[path] = os.stat(path).st_ino
+            if inodes == self.last_inodes:
+                # Continue
+                return False
+            self.last_inodes = inodes
 
         # Save a backtrace in case the test does fail
         backtrace = gdb.execute('backtrace', to_string=True)
index b31fe622c9b541305aaea24a4134fd532c26592c..9f1e91f0373c093f95cff3403203db3721487d25 100644 (file)
@@ -5,6 +5,7 @@ Date: Tue, 17 Nov 2009 14:00:54 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ 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
@@ -39,6 +40,7 @@ Cheers,
 
   [ multipart/mixed ]
   [ multipart/signed ]
+  [ Unknown signature status ]
   [ text/plain ]
   > See the patch just posted here.
 
@@ -65,6 +67,7 @@ Cheers,
 
     [ multipart/mixed ]
     [ multipart/signed ]
+    [ Unknown signature status ]
     [ text/plain ]
     > I've also pushed a slightly more complicated (and complete) fix to my
     > private notmuch repository
index bafb479ece3640f55745a5b4bae926a236c10223..118053ba1b496b2710a2e440c3542629e06d0657 100644 (file)
@@ -5,6 +5,7 @@ Date: Tue, 17 Nov 2009 14:00:54 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ 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
@@ -38,6 +39,7 @@ Cheers,
 
   [ multipart/mixed ]
   [ multipart/signed ]
+  [ Unknown signature status ]
   [ text/plain ]
   > See the patch just posted here.
 
@@ -62,6 +64,7 @@ Cheers,
 
     [ multipart/mixed ]
     [ multipart/signed ]
+    [ Unknown signature status ]
     [ text/plain ]
     > I've also pushed a slightly more complicated (and complete) fix to my
     > private notmuch repository
index 37b4f7de1d4646634cde6007c45f1326dfd89468..2cb1211800362c241c3c9421972c1c4c5cd3b051 100644 (file)
@@ -5,6 +5,7 @@ Date: Tue, 17 Nov 2009 14:00:54 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ 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
@@ -39,6 +40,7 @@ Date: Tue, 17 Nov 2009 15:33:01 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ text/plain ]
 > See the patch just posted here.
 
@@ -65,6 +67,7 @@ Date: Tue, 17 Nov 2009 19:50:40 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ text/plain ]
 > I've also pushed a slightly more complicated (and complete) fix to my
 > private notmuch repository
index 3282c7b148d90a8f3d712a6aefb2d4628cd8c0e4..ce2892a05f3af32c8f01486c889918c5a17cbc7d 100644 (file)
@@ -9,6 +9,7 @@ Subject: [notmuch] Working with Maildir storage?
 
   [ multipart/mixed ]
   [ multipart/signed ]
+  [ Unknown signature status ]
   [ text/plain ]
   > See the patch just posted here.
 
index 9ba4cfc1d560e4c33db3b296924ddb8e834f81cb..89186082f8a933bc28b8be2598eb7e140485e5f7 100644 (file)
@@ -8,8 +8,5 @@ Search:                                                                     .
 
 All tags: [show]
 
-        Type a search query and hit RET to view matching threads.
-               Edit saved searches with the `edit' button.
-  Hit RET or click on a saved search or tag name to view matching threads.
-      `=' to refresh this screen. `s' to search messages. `q' to quit.
-                           Customize this page.
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
index 1c8d6eb624b97237ad458d5ed61cadc666521dcf..da0f3525325dc7622ed1d989cd7280f76dc2bbe0 100644 (file)
@@ -11,8 +11,5 @@ All tags: [hide]
          52 a-very-long-tag       52 inbox                 52 unread
           4 attachment             7 signed
 
-        Type a search query and hit RET to view matching threads.
-               Edit saved searches with the `edit' button.
-  Hit RET or click on a saved search or tag name to view matching threads.
-      `=' to refresh this screen. `s' to search messages. `q' to quit.
-                           Customize this page.
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
index 05475b15abee9c81a4fc30907438c83c7fd73e23..939965fef154b83595e6c7c140cd39e8076e13e8 100644 (file)
@@ -4,8 +4,5 @@ Search:                                                                     .
 
 All tags: [show]
 
-        Type a search query and hit RET to view matching threads.
-               Edit saved searches with the `edit' button.
-  Hit RET or click on a saved search or tag name to view matching threads.
-      `=' to refresh this screen. `s' to search messages. `q' to quit.
-                           Customize this page.
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
index 854e0c2a22e891c2e00c5c7a426208af324cf604..97d7db26ebfa953cca19077ce7561c910dd3f2b4 100644 (file)
@@ -8,8 +8,5 @@ Search:                                                                     .
 
 All tags: [show]
 
-        Type a search query and hit RET to view matching threads.
-               Edit saved searches with the `edit' button.
-  Hit RET or click on a saved search or tag name to view matching threads.
-      `=' to refresh this screen. `s' to search messages. `q' to quit.
-                           Customize this page.
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
index cdbfa1d7e18476c32ca8483936b061bacf09d674..359989228b5a7a768e15ad15420c3e232ada583f 100644 (file)
@@ -5,6 +5,7 @@ Date: Tue, 17 Nov 2009 14:00:54 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ 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
@@ -45,6 +46,7 @@ http://notmuchmail.org/mailman/listinfo/notmuch
 
  [ multipart/mixed ]
  [ multipart/signed ]
+ [ Unknown signature status ]
  [ text/plain ]
 
  Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did
@@ -77,6 +79,7 @@ http://notmuchmail.org/mailman/listinfo/notmuch
 
   [ multipart/mixed ]
   [ multipart/signed ]
+  [ Unknown signature status ]
   [ text/plain ]
   > See the patch just posted here.
 
@@ -159,6 +162,7 @@ http://notmuchmail.org/mailman/listinfo/notmuch
 
     [ multipart/mixed ]
     [ multipart/signed ]
+    [ Unknown signature status ]
     [ text/plain ]
     > I've also pushed a slightly more complicated (and complete) fix to my
     > private notmuch repository
index b0bf93edc99146050a21e6b1e8ab3bc354b4ff15..4721b8b0875d5267d7907d73f80f8165dc538fb6 100644 (file)
@@ -5,6 +5,7 @@ Date: Tue, 17 Nov 2009 14:00:54 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ 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
@@ -45,6 +46,7 @@ http://notmuchmail.org/mailman/listinfo/notmuch
 
     [ multipart/mixed ]
     [ multipart/signed ]
+    [ Unknown signature status ]
     [ text/plain ]
 
     Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did
@@ -77,6 +79,7 @@ http://notmuchmail.org/mailman/listinfo/notmuch
 
        [ multipart/mixed ]
        [ multipart/signed ]
+       [ Unknown signature status ]
        [ text/plain ]
        > See the patch just posted here.
 
@@ -159,6 +162,7 @@ http://notmuchmail.org/mailman/listinfo/notmuch
 
                [ multipart/mixed ]
                [ multipart/signed ]
+               [ Unknown signature status ]
                [ text/plain ]
                > I've also pushed a slightly more complicated (and complete) fix to my
                > private notmuch repository
index 08de8b5da86a9fd9cb2b873e02b59744018f70f6..62a463535e5bbe3231994959c8913be7731f668f 100644 (file)
@@ -5,6 +5,7 @@ Date: Tue, 17 Nov 2009 14:00:54 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ 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
@@ -45,6 +46,7 @@ Date: Wed, 18 Nov 2009 01:02:38 +0600
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ text/plain ]
 
 Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did
@@ -77,6 +79,7 @@ Date: Tue, 17 Nov 2009 15:33:01 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ text/plain ]
 > See the patch just posted here.
 
@@ -159,6 +162,7 @@ Date: Tue, 17 Nov 2009 19:50:40 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ text/plain ]
 > I've also pushed a slightly more complicated (and complete) fix to my
 > private notmuch repository
diff --git a/test/ghost-report.cc b/test/ghost-report.cc
new file mode 100644 (file)
index 0000000..3e1b07c
--- /dev/null
@@ -0,0 +1,14 @@
+#include <iostream>
+#include <cstdlib>
+#include <xapian.h>
+
+int main(int argc, char **argv) {
+
+    if (argc < 2) {
+       std::cerr << "usage: ghost-report xapian-dir" << std::endl;
+       exit(1);
+    }
+
+    Xapian::Database db(argv[1]);
+    std::cout << db.get_termfreq("Tghost") << std::endl;
+}
diff --git a/test/smime/README b/test/smime/README
new file mode 100644 (file)
index 0000000..92803c7
--- /dev/null
@@ -0,0 +1,7 @@
+test.crt: self signed certificated
+    % gpgsm --gen-key # needs gpgsm 2.1
+
+key+cert.pem: cert + unencryped private
+    % gpsm --import test.crt
+    % gpgsm --export-private-key-p12 -out foo.p12  (no passphrase)
+    % openssl pkcs12 -in ns.p12 -clcerts -nodes > key+cert.pem
diff --git a/test/smime/key+cert.pem b/test/smime/key+cert.pem
new file mode 100644 (file)
index 0000000..6ee30cf
--- /dev/null
@@ -0,0 +1,56 @@
+Bag Attributes
+    friendlyName: GnuPG exported certificate e0972a47
+    localKeyID: 61 6F 46 CD 73 83 4C 63 84 77 56 AF 0D FB 64 A6 E0 97 2A 47 
+subject=/CN=Notmuch Test Suite
+issuer=/CN=Notmuch Test Suite
+-----BEGIN CERTIFICATE-----
+MIIDCzCCAfOgAwIBAgIIb3SMlL0MZ6kwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UE
+AxMSTm90bXVjaCBUZXN0IFN1aXRlMCAXDTE1MTIxNDAyMDgxMFoYDzIwNjMwNDA1
+MTcwMDAwWjAdMRswGQYDVQQDExJOb3RtdWNoIFRlc3QgU3VpdGUwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7vH1/lkENTAJRbyq2036K7Pw+imSIhB5T
+U0WnAgVGWOemY1Eppi9Dk6rjDxuuUKOCQ5el2wmFZN57Fi/4leBH7x217BnnqWNU
+QV88DxEfV+sk8dSb4a5FOOyfhFJmZso/0lK8x0fBcCNjmRFIjB1afSSXWnCvRpAR
+v+O9trLJuIjbbmXg1gltjuB5yDw8/OLEI7G7YSIop9FxopWJL5rW/o2WEfRPGpYe
+HNRLObCRIvbyDd6XjaCrKBuIrhN7R7mmIa9PUyl8TiY+pCMWs9dHmOsiC73/+P6E
+AhsTOY1bfbGQXBAGZ/FL+SgC5wEcPr2u3+y8y5gw2bpaVhQnu6YLAgMBAAGjTTBL
+MCUGA1UdEQQeMByBGnRlc3Rfc3VpdGVAbm90bXVjaG1haWwub3JnMBEGCisGAQQB
+2kcCAgEEAwEB/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBb
+XP5OnRVplrEdlnivx3CbCLWO13fcMWXfvKxLGsKFwKuxtpvINFUKM+jDr0kVdQ3d
+u3DJe2hNFQMILK/KrGyN5qEz2YBdHNvdkkvWA+3WHr/tiNr6Rly6QuxBzouxzmRu
+MmnUhsOzZaHT3GmLSVJlwie8KqSfKVGwyBmCyHbUQkMrSEV6QDESN6KyWt85gokB
+56Bc/wVq073xS1nFbfF1M3Z5q5BlLZK4IOerKTQx/oSfR4EX6B7rW2pttWsUCyEj
+LljaA8ehxR9B29m08IGGl43pHEpC1WnOHvsEGs99mPpjWbUgVv5KY7OuS/8iVw6v
+/Yy5Z+JBwlMzTBaUXXl3
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: GnuPG exported certificate e0972a47
+    localKeyID: 61 6F 46 CD 73 83 4C 63 84 77 56 AF 0D FB 64 A6 E0 97 2A 47 
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7vH1/lkENTAJR
+byq2036K7Pw+imSIhB5TU0WnAgVGWOemY1Eppi9Dk6rjDxuuUKOCQ5el2wmFZN57
+Fi/4leBH7x217BnnqWNUQV88DxEfV+sk8dSb4a5FOOyfhFJmZso/0lK8x0fBcCNj
+mRFIjB1afSSXWnCvRpARv+O9trLJuIjbbmXg1gltjuB5yDw8/OLEI7G7YSIop9Fx
+opWJL5rW/o2WEfRPGpYeHNRLObCRIvbyDd6XjaCrKBuIrhN7R7mmIa9PUyl8TiY+
+pCMWs9dHmOsiC73/+P6EAhsTOY1bfbGQXBAGZ/FL+SgC5wEcPr2u3+y8y5gw2bpa
+VhQnu6YLAgMBAAECggEAVhtHCHz3C01Ahu9RDRgGI1w8+cZqA/9tFVTNTqNrne9r
+GHLXKB4z8W/KYmhsjtAnnri31neXb1prfNMZX5AGlZfD7cwDubCEgYGWV6qldNXT
+YVeV54VkdBV+2k9Lp/Ifc5RZJILWk4+Ge8kaF0dEs1tQrCbsJkhcDfgQUdR5PnGe
+6cKv/8HJo0ep6u5cJloIluit8yF3z4+aHixMQBvQKm/8tug+EsrQZ3IVXbh1hONO
+AZ68z9CrU2pJ/0w/jwwcM5feRfTMC7bZ3vkQb1mQKYFJrvN77TGroUtAZFWqJw7M
+r0f2MShdVjfEdJ1ySnCyKF24cSSPSQsLZUe4UlFyQQKBgQDlqr9ajaUzc6Lyma2e
+Q1IJapbX2OZQtf5tlKVCVtZOlu5r97YMOK96XsQFKtdxhAhrGvvTJwPmwhj+fqfR
+XltNrmUBpHCMsm9nloADvBS83KTP5tw9TMT0VZpt+m5XmvutdyQbSKwy+KMy+GZz
+/XBQCfTEoiDS4grGFftvZuRB4QKBgQDRQvsVFMh2NOnVGqczHJNGjvbDueUJmPUN
+3VxZc/FpBGLRSoN7uxQ4dGNnwyvXHs+pLAAC6xZpFCos9c3R8EPvoMyUehoDSAKW
+CMD4C+K8z7n4ducE5a0NrGIgQvnXtteKr3ZwK8V7cscyTCyjXdrQmQ5XHeue8asR
+758g+dG9awKBgEWuZJho2XKe5xWMIu0dp8pLmLCsklRyo1tD+lACYMs/Z99CLO3Q
+VQ1fq0GWGf/K+3LjoPwTnk9pHIQ6kVgotLMA8oxpA+zsRni7ZOO9MN2MZETf2nqO
+zEMFpfEwRkI2N54Nw9qzVeuxHHLegtc2Udk27BisyCCzjGlFSiAmq6KBAoGAFGfE
+RXjcvT65HX8Gaya+wtugFB8BRx0JX7dI6OLk5ZKLmq0ykH2bQepgnWermmU4we77
+0Dvtfa3u0YjZ/24XXg2YbSpWiWps0Y2/C7AyAAzq12/1OGcX5qk4Tbd0f+QkIset
+qxzmt4XcAKw50J+Vf3DmbYQ1M/BftCZcTm0ShHcCgYEAxp8mjE8iIHxFrm7nHMS0
+2/iWxO8DYaAZ0OLfjaZELHchVvTwa+DynbkwvOc3l4cbNTVaf9O6nmHTkLyBLBNr
+2htPKm1vi9TzNdvGqobFO3ijfvdGvq1rjQl86ns0cf395REmEaVX3zcw2v+GyC5n
+qE6Aa5bvdZ9Yykg6aoFo1mY=
+-----END PRIVATE KEY-----
diff --git a/test/smime/test.crt b/test/smime/test.crt
new file mode 100644 (file)
index 0000000..e5d1e82
--- /dev/null
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCzCCAfOgAwIBAgIIb3SMlL0MZ6kwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UE
+AxMSTm90bXVjaCBUZXN0IFN1aXRlMCAXDTE1MTIxNDAyMDgxMFoYDzIwNjMwNDA1
+MTcwMDAwWjAdMRswGQYDVQQDExJOb3RtdWNoIFRlc3QgU3VpdGUwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7vH1/lkENTAJRbyq2036K7Pw+imSIhB5T
+U0WnAgVGWOemY1Eppi9Dk6rjDxuuUKOCQ5el2wmFZN57Fi/4leBH7x217BnnqWNU
+QV88DxEfV+sk8dSb4a5FOOyfhFJmZso/0lK8x0fBcCNjmRFIjB1afSSXWnCvRpAR
+v+O9trLJuIjbbmXg1gltjuB5yDw8/OLEI7G7YSIop9FxopWJL5rW/o2WEfRPGpYe
+HNRLObCRIvbyDd6XjaCrKBuIrhN7R7mmIa9PUyl8TiY+pCMWs9dHmOsiC73/+P6E
+AhsTOY1bfbGQXBAGZ/FL+SgC5wEcPr2u3+y8y5gw2bpaVhQnu6YLAgMBAAGjTTBL
+MCUGA1UdEQQeMByBGnRlc3Rfc3VpdGVAbm90bXVjaG1haWwub3JnMBEGCisGAQQB
+2kcCAgEEAwEB/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBb
+XP5OnRVplrEdlnivx3CbCLWO13fcMWXfvKxLGsKFwKuxtpvINFUKM+jDr0kVdQ3d
+u3DJe2hNFQMILK/KrGyN5qEz2YBdHNvdkkvWA+3WHr/tiNr6Rly6QuxBzouxzmRu
+MmnUhsOzZaHT3GmLSVJlwie8KqSfKVGwyBmCyHbUQkMrSEV6QDESN6KyWt85gokB
+56Bc/wVq073xS1nFbfF1M3Z5q5BlLZK4IOerKTQx/oSfR4EX6B7rW2pttWsUCyEj
+LljaA8ehxR9B29m08IGGl43pHEpC1WnOHvsEGs99mPpjWbUgVv5KY7OuS/8iVw6v
+/Yy5Z+JBwlMzTBaUXXl3
+-----END CERTIFICATE-----
index 5eb618c1f51aabffb562d4b2ac9dc6e646b668f7..4e17b781e26298c3abefec9ec7d2c1e7e9e28644 100644 (file)
@@ -48,7 +48,7 @@ restore_database () {
 
 # Test the binaries we have just built.  The tests are kept in
 # test/ subdirectory and are run in 'trash directory' subdirectory.
-TEST_DIRECTORY=$(pwd)
+TEST_DIRECTORY=$(pwd -P)
 notmuch_path=`find_notmuch_path "$TEST_DIRECTORY"`
 
 # configure output
index 04c8d63450533dbf566ff25cb6f2276b2032ee5b..596a705103ef1a3bd1c7902fa7d58f97f9902344 100644 (file)
@@ -188,3 +188,13 @@ nothing."
 ;; environments
 
 (setq mm-text-html-renderer 'html2text)
+
+;; Set some variables for S/MIME tests.
+
+(setq smime-keys '(("" "test_suite.pem" nil)))
+
+(setq mml-smime-use 'openssl)
+
+;; all test keys are without passphrase
+(eval-after-load 'smime
+  '(defun smime-ask-passphrase (cache)  nil))
index 126911fb385aacb7aa087ab9c7c099419c7641fd..ac04b15a96ba1da2c1c36759a5b78c030602cd0e 100644 (file)
@@ -82,6 +82,9 @@ unset CDPATH
 
 unset GREP_OPTIONS
 
+# For emacsclient
+unset ALTERNATE_EDITOR
+
 # Convenience
 #
 # A regexp to match 5 and 40 hexdigits
@@ -675,9 +678,14 @@ notmuch_search_sanitize ()
     perl -pe 's/("?thread"?: ?)("?)................("?)/\1\2XXX\3/'
 }
 
-notmuch_search_files_sanitize()
+notmuch_search_files_sanitize ()
 {
-    sed -e "s,$MAIL_DIR,MAIL_DIR,"
+    notmuch_dir_sanitize
+}
+
+notmuch_dir_sanitize ()
+{
+    sed -e "s,$MAIL_DIR,MAIL_DIR," -e "s,${PWD},CWD,g" "$@"
 }
 
 NOTMUCH_SHOW_FILENAME_SQUELCH='s,filename:.*/mail,filename:/XXX/mail,'
@@ -1177,7 +1185,7 @@ test_C () {
     echo "== stdout ==" > OUTPUT.stdout
     echo "== stderr ==" > OUTPUT.stderr
     ./${exec_file} "$@" 1>>OUTPUT.stdout 2>>OUTPUT.stderr
-    sed "s,${PWD},CWD,g"  OUTPUT.stdout OUTPUT.stderr > OUTPUT
+    notmuch_dir_sanitize OUTPUT.stdout OUTPUT.stderr > OUTPUT
 }
 
 
@@ -1319,10 +1327,23 @@ test -z "$NO_PYTHON" && test_set_prereq PYTHON
 ln -s x y 2>/dev/null && test -h y 2>/dev/null && test_set_prereq SYMLINKS
 rm -f y
 
+# convert variable from configure to more convenient form
+case "$NOTMUCH_DEFAULT_XAPIAN_BACKEND" in
+    glass)
+       db_ending=glass
+    ;;
+    chert)
+       db_ending=DB
+    ;;
+    *)
+       error "Unknown Xapian backend $NOTMUCH_DEFAULT_XAPIAN_BACKEND"
+esac
 # declare prerequisites for external binaries used in tests
 test_declare_external_prereq dtach
 test_declare_external_prereq emacs
 test_declare_external_prereq ${TEST_EMACSCLIENT}
 test_declare_external_prereq gdb
 test_declare_external_prereq gpg
+test_declare_external_prereq openssl
+test_declare_external_prereq gpgsm
 test_declare_external_prereq ${NOTMUCH_PYTHON}
index e16792b886c9cef47a2938324a991f73d29d736e..ab7205b7b8dd7d1d861ec044806882ac5ab58115 100644 (file)
@@ -5,6 +5,7 @@ Date: Tue, 17 Nov 2009 14:00:54 -0500
 
 [ multipart/mixed ]
 [ multipart/signed ]
+[ Unknown signature status ]
 [ 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
diff --git a/version b/version
index 5320adc1c9b6523c3a30e1ff211c5cea7319ca03..e3462940604b0ed68ebec6603d5b0a0cabd3c4b3 100644 (file)
--- a/version
+++ b/version
@@ -1 +1 @@
-0.21
+0.22