+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)
=========================
...
},
...
- },
+ }
Python Bindings
---------------
LIBNOTMUCH="../../lib/$(LINKER_NAME)" \
ruby extconf.rb --vendor
$(MAKE) -C $(dir)/ruby
-else
- @echo Missing dependency, skipping ruby bindings
endif
CLEAN += $(patsubst %,$(dir)/ruby/%, \
-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
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
.. 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
------------------------------------------------------
: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:
.. 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__
UnbalancedFreezeThawError,
UnbalancedAtomicError,
NotInitializedError,
+ UnsupportedOperationError,
+ UpgradeRequiredError,
+ PathError,
)
from .version import __VERSION__
__LICENSE__ = "GPL v3+"
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
NotmuchError,
NullPointerError,
NotInitializedError,
- ReadOnlyDatabaseError,
)
from .message import Message
from .tag import Tags
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
"""
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() + "'"
'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
* 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
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)
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
from ctypes import c_char_p
from .globals import (
nmlib,
- NotmuchMessageP,
NotmuchFilenamesP,
Python3StringMixIn,
)
as well as::
- number_of_names = len(names)
+ list_of_names = list(names)
and even a simple::
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
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):
NotmuchMessagesP,
)
from .errors import (
+ NotmuchError,
NullPointerError,
NotInitializedError,
)
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
: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
: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
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.
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.
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
# this file should be kept in sync with ../../../version
-__VERSION__ = '0.21'
+__VERSION__ = '0.22'
SOVERSION = '4'
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
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}
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
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... "
# 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}
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}
\$(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) \\
\$(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) \\
# 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))
/* 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);
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");
* 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
crypto->gpgctx = NULL;
}
+ if (crypto->pkcs7ctx) {
+ g_object_unref (crypto->pkcs7ctx);
+ crypto->pkcs7ctx = NULL;
+ }
+
return 0;
}
+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.
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,
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/
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
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;
* 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.
--- /dev/null
+*.pyc
+_build
--- /dev/null
+# 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
--- /dev/null
+# -*- 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'),
+]
--- /dev/null
+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`
--- /dev/null
+==============
+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)**
+
--- /dev/null
+==============
+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)**
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):])
+++ /dev/null
-#!/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'])
--- /dev/null
+#!/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'])
--- /dev/null
+{
+ "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"
+ }
+ ]
+}
+++ /dev/null
-{
- "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"
- }
- ]
-}
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
--- /dev/null
+#!/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:
# 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__)
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
$(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
$(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
t)))
(provide 'coolj)
+
+;;; coolj.el ends here
;;
;; Authors: Austin Clements <aclements@csail.mit.edu>
+;;; Code:
+
(defun batch-make-deps ()
"Invoke `make-deps' for each file on the command line."
(file-name-sans-extension
(file-relative-name fname dir)))))))))
(end-of-file nil))))
+
+;;; make-deps.el ends here
-;; 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)
: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)
(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
--- /dev/null
+;;; 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
-;; notmuch-crypto.el --- functions for handling display of cryptographic metadata.
+;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata.
;;
;; Copyright © Jameson Rollins
;;
;;
;; Authors: Jameson Rollins <jrollins@finestructure.net>
+;;; Code:
+
(require 'notmuch-lib)
(defcustom notmuch-crypto-process-mime nil
(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
((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
;;
(provide 'notmuch-crypto)
+
+;;; notmuch-crypto.el ends here
-;; notmuch-hello.el --- welcome to notmuch, a frontend
+;;; notmuch-hello.el --- welcome to notmuch, a frontend
;;
;; Copyright © David Edmondson
;;
;;
;; Authors: David Edmondson <dme@dme.org>
+;;; Code:
+
(eval-when-compile (require 'cl))
(require 'widget)
(require 'wid-edit) ; For `widget-forward'.
(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.
(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}"
(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)))))
;;
(provide 'notmuch-hello)
+
+;;; notmuch-hello.el ends here
-;; notmuch-jump.el --- User-friendly shortcut keys
+;;; notmuch-jump.el --- User-friendly shortcut keys
;;
;; Copyright © Austin Clements
;;
;; Authors: Austin Clements <aclements@csail.mit.edu>
;; David Edmondson <dme@dme.org>
+;;; Code:
+
(eval-when-compile (require 'cl))
(require 'notmuch-lib)
;;
(provide 'notmuch-jump)
+
+;;; notmuch-jump.el ends here
-;; notmuch-lib.el --- common variables, functions and function declarations
+;;; notmuch-lib.el --- common variables, functions and function declarations
;;
;; Copyright © Carl Worth
;;
;; This is an part of an emacs-based interface to the notmuch mail system.
+;;; Code:
+
(require 'mm-view)
(require 'mm-decode)
(require 'cl)
"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.
(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.
"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))))))
(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.
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
;; Local Variables:
;; byte-compile-warnings: (not cl-functions)
;; End:
+
+;;; notmuch-lib.el ends here
+;;; 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)
(provide 'notmuch-maildir-fcc)
+;;; notmuch-maildir-fcc.el ends here
-;; notmuch-message.el --- message-mode functions specific to notmuch
+;;; notmuch-message.el --- message-mode functions specific to notmuch
;;
;; Copyright © Jesse Rosenthal
;;
;;
;; Authors: Jesse Rosenthal <jrosenthal@jhu.edu>
+;;; Code:
+
(require 'message)
(require 'notmuch-tag)
(require 'notmuch-mua)
(add-hook 'message-send-hook 'notmuch-message-mark-replied)
(provide 'notmuch-message)
+
+;;; notmuch-message.el ends here
-;; notmuch-mua.el --- emacs style mail-user-agent
+;;; notmuch-mua.el --- emacs style mail-user-agent
;;
;; Copyright © David Edmondson
;;
;;
;; Authors: David Edmondson <dme@dme.org>
+;;; Code:
+
(require 'message)
(require 'mm-view)
(require 'format-spec)
(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))
;;
: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 ()
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,
(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)
(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
(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)
(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)
(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.
;;
(provide 'notmuch-mua)
+
+;;; notmuch-mua.el ends here
-;; notmuch-parser.el --- streaming S-expression parser
+;;; notmuch-parser.el --- streaming S-expression parser
;;
;; Copyright © Austin Clements
;;
;;
;; Authors: Austin Clements <aclements@csail.mit.edu>
+;;; Code:
+
(require 'cl)
(defun notmuch-sexp-create-parser ()
;; Local Variables:
;; byte-compile-warnings: (not cl-functions)
;; End:
+
+;;; notmuch-parser.el ends here
-;; notmuch-print.el --- printing messages from notmuch.
+;;; notmuch-print.el --- printing messages from notmuch.
;;
;; Copyright © David Edmondson
;;
;;
;; Authors: David Edmondson <dme@dme.org>
+;;; Code:
+
(require 'notmuch-lib)
(declare-function notmuch-show-get-prop "notmuch-show" (prop &optional props))
(funcall notmuch-print-mechanism msg))
(provide 'notmuch-print)
+
+;;; notmuch-print.el ends here
-;; notmuch-query.el --- provide an emacs api to query notmuch
+;;; notmuch-query.el --- provide an emacs api to query notmuch
;;
;; Copyright © David Bremner
;;
;;
;; Authors: David Bremner <david@tethera.net>
+;;; Code:
+
(require 'notmuch-lib)
(defun notmuch-query-get-threads (search-terms)
(notmuch-query-get-threads search-terms)))
(provide 'notmuch-query)
+
+;;; notmuch-query.el ends here
-;; notmuch-show.el --- displaying notmuch forests.
+;;; notmuch-show.el --- displaying notmuch forests.
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
;; Authors: Carl Worth <cworth@cworth.org>
;; David Edmondson <dme@dme.org>
+;;; Code:
+
(eval-when-compile (require 'cl))
(require 'mm-view)
(require 'message)
(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
'message-header-cc)
((looking-at "[Ss]ubject:")
'message-header-subject)
- ((looking-at "[Ff]rom:")
- 'message-header-from)
(t
'message-header-other))))
(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
(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,
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)))
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)))
;; \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"
;; 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)))
(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.
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.
(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)))
(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)
(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.
"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
(provide 'notmuch-show)
+
+;;; notmuch-show.el ends here
-;; notmuch-tag.el --- tag messages within emacs
+;;; notmuch-tag.el --- tag messages within emacs
;;
;; Copyright © Damien Cassou
;; Copyright © Carl Worth
-;; notmuch-tree.el --- displaying notmuch forests.
+;;; notmuch-tree.el --- displaying notmuch forests.
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
;; Authors: David Edmondson <dme@dme.org>
;; Mark Walters <markwalters1009@gmail.com>
+;;; Code:
+
(require 'mail-parse)
(require 'notmuch-lib)
;;
(provide 'notmuch-tree)
+
+;;; notmuch-tree.el ends here
+;;; 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
-;; notmuch-wash.el --- cleaning up message bodies
+;;; notmuch-wash.el --- cleaning up message bodies
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
;; 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))
;;
(provide 'notmuch-wash)
+
+;;; notmuch-wash.el ends here
-;; notmuch.el --- run notmuch within emacs
+;;; notmuch.el --- run notmuch within emacs
;;
;; Copyright © Carl Worth
;;
;; 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.
;;
;; 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)
(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)
(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)))
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."
(let ((init-file (locate-file notmuch-init-file '("/")
(get-load-suffixes))))
(if init-file (load init-file nil t t))))
+
+;;; notmuch.el ends here
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) {
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;
}
/* Finally, skip multiple slashes. */
- while (slash != path) {
- if (*slash != '/')
- break;
-
+ while (slash != path && *(slash - 1) == '/')
--slash;
- }
if (slash == path) {
if (directory)
*basename = path;
} else {
if (directory)
- *directory = talloc_strndup (ctx, path, slash - path + 1);
+ *directory = talloc_strndup (ctx, path, slash - path);
}
return NOTMUCH_STATUS_SUCCESS;
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));
* 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)
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
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. */
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)
{
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,
_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
#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__ */
/**
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
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;
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;
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;
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 |
#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"
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;
/* 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:
*
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;
printf -v $2 '%s' "${__escape_arg__//\"/\\\"}"
}
-EMACS=${EMACS-emacs}
-EMACSCLIENT=${EMACSCLIENT-emacsclient}
+EMACS=${EMACS:-emacs}
+EMACSCLIENT=${EMACSCLIENT:-emacsclient}
PRINT_ONLY=
NO_WINDOW=
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:
*/
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;
}
/* 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;
}
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);
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);
"%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);
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);
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));
* 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";
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";
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) {
}
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;
+}
symbol-test
make-db-version
test-results
+ghost-report
tmp.*
$(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 \
$(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
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.
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)
- 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:
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.
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
#
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."
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."
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."
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."
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."
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
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."
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
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"
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
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
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
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
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
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
--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
> 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
--- /dev/null
+#!/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
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
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
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>
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) {
--- /dev/null
+#!/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
--- /dev/null
+#!/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
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)
[ 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
[ multipart/mixed ]
[ multipart/signed ]
+ [ Unknown signature status ]
[ text/plain ]
> See the patch just posted here.
[ 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
[ 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
[ multipart/mixed ]
[ multipart/signed ]
+ [ Unknown signature status ]
[ text/plain ]
> See the patch just posted here.
[ 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
[ 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
[ multipart/mixed ]
[ multipart/signed ]
+[ Unknown signature status ]
[ text/plain ]
> See the patch just posted here.
[ 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
[ multipart/mixed ]
[ multipart/signed ]
+ [ Unknown signature status ]
[ text/plain ]
> See the patch just posted here.
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.
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.
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.
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.
[ 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
[ 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
[ multipart/mixed ]
[ multipart/signed ]
+ [ Unknown signature status ]
[ text/plain ]
> See the patch just posted here.
[ 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
[ 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
[ 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
[ multipart/mixed ]
[ multipart/signed ]
+ [ Unknown signature status ]
[ text/plain ]
> See the patch just posted here.
[ 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
[ 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
[ 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
[ multipart/mixed ]
[ multipart/signed ]
+[ Unknown signature status ]
[ text/plain ]
> See the patch just posted here.
[ 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
--- /dev/null
+#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;
+}
--- /dev/null
+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
--- /dev/null
+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-----
--- /dev/null
+-----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-----
# 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
;; 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))
unset GREP_OPTIONS
+# For emacsclient
+unset ALTERNATE_EDITOR
+
# Convenience
#
# A regexp to match 5 and 40 hexdigits
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,'
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
}
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}
[ 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