+++ /dev/null
-"""Pythonic API to the notmuch database.
-
-Creating Objects
-================
-
-Only the :class:`Database` object is meant to be created by the user.
-All other objects should be created from this initial object. Users
-should consider their signatures implementation details.
-
-Errors
-======
-
-All errors occuring due to errors from the underlying notmuch database
-are subclasses of the :exc:`NotmuchError`. Due to memory management
-it is possible to try and use an object after it has been freed. In
-this case a :exc:`ObjectDestoryedError` will be raised.
-
-Memory Management
-=================
-
-Libnotmuch uses a hierarchical memory allocator, this means all
-objects have a strict parent-child relationship and when the parent is
-freed all the children are freed as well. This has some implications
-for these Python bindings as parent objects need to be kept alive.
-This is normally schielded entirely from the user however and the
-Python objects automatically make sure the right references are kept
-alive. It is however the reason the :class:`BaseObject` exists as it
-defines the API all Python objects need to implement to work
-correctly.
-
-Collections and Containers
-==========================
-
-Libnotmuch exposes nearly all collections of things as iterators only.
-In these python bindings they have sometimes been exposed as
-:class:`collections.abc.Container` instances or subclasses of this
-like :class:`collections.abc.Set` or :class:`collections.abc.Mapping`
-etc. This gives a more natural API to work with, e.g. being able to
-treat tags as sets. However it does mean that the
-:meth:`__contains__`, :meth:`__len__` and frieds methods on these are
-usually more and essentially O(n) rather than O(1) as you might
-usually expect from Python containers.
-"""
-
-from notdb import _capi
-from notdb._base import *
-from notdb._database import *
-from notdb._errors import *
-from notdb._message import *
-from notdb._tags import *
-from notdb._thread import *
-
-
-NOTMUCH_TAG_MAX = _capi.lib.NOTMUCH_TAG_MAX
-del _capi
-
-
-# Re-home all the objects to the package. This leaves __qualname__ intact.
-for x in locals().copy().values():
- if hasattr(x, '__module__'):
- x.__module__ = __name__
-del x
+++ /dev/null
-import abc
-import collections.abc
-
-from notdb import _capi as capi
-from notdb import _errors as errors
-
-
-__all__ = ['NotmuchObject', 'BinString']
-
-
-class NotmuchObject(metaclass=abc.ABCMeta):
- """Base notmuch object syntax.
-
- This base class exists to define the memory management handling
- required to use the notmuch library. It is meant as an interface
- definition rather than a base class, though you can use it as a
- base class to ensure you don't forget part of the interface. It
- only concerns you if you are implementing this package itself
- rather then using it.
-
- libnotmuch uses a hierarchical memory allocator, where freeing the
- memory of a parent object also frees the memory of all child
- objects. To make this work seamlessly in Python this package
- keeps references to parent objects which makes them stay alive
- correctly under normal circumstances. When an object finally gets
- deleted the :meth:`__del__` method will be called to free the
- memory.
-
- However during some peculiar situations, e.g. interpreter
- shutdown, it is possible for the :meth:`__del__` method to have
- been called, whele there are still references to an object. This
- could result in child objects asking their memeory to be freed
- after the parent has already freed the memory, making things
- rather unhappy as double frees are not taken lightly in C. To
- handle this case all objects need to follow the same protocol to
- destroy themselves, see :meth:`destroy`.
-
- Once an object has been destroyed trying to use it should raise
- the :exc:`ObjectDestroyedError` exception. For this see also the
- convenience :class:`MemoryPointer` descriptor in this module which
- can be used as a pointer to libnotmuch memory.
- """
-
- @abc.abstractmethod
- def __init__(self, parent, *args, **kwargs):
- """Create a new object.
-
- Other then for the toplevel :class:`Database` object
- constructors are only ever called by internal code and not by
- the user. Per convention their signature always takes the
- parent object as first argument. Feel free to make the rest
- of the signature match the object's requirement. The object
- needs to keep a reference to the parent, so it can check the
- parent is still alive.
- """
-
- @property
- @abc.abstractmethod
- def alive(self):
- """Whether the object is still alive.
-
- This indicates whether the object is still alive. The first
- thing this needs to check is whether the parent object is
- still alive, if it is not then this object can not be alive
- either. If the parent is alive then it depends on whether the
- memory for this object has been freed yet or not.
- """
-
- def __del__(self):
- self._destroy()
-
- @abc.abstractmethod
- def _destroy(self):
- """Destroy the object, freeing all memory.
-
- This method needs to destory the object on the
- libnotmuch-level. It must ensure it's not been destroyed by
- it's parent object yet before doing so. It also must be
- idempotent.
- """
-
-
-class MemoryPointer:
- """Data Descriptor to handle accessing libnotmuch pointers.
-
- Most :class:`NotmuchObject` instances will have one or more CFFI
- pointers to C-objects. Once an object is destroyed this pointer
- should no longer be used and a :exc:`ObjectDestroyedError`
- exception should be raised on trying to access it. This
- descriptor simplifies implementing this, allowing the creation of
- an attribute which can be assigned to, but when accessed when the
- stored value is *None* it will raise the
- :exc:`ObjectDestroyedError` exception::
-
- class SomeOjb:
- _ptr = MemoryPointer()
-
- def __init__(self, ptr):
- self._ptr = ptr
-
- def destroy(self):
- somehow_free(self._ptr)
- self._ptr = None
-
- def do_something(self):
- return some_libnotmuch_call(self._ptr)
- """
-
- def __get__(self, instance, owner):
- try:
- val = getattr(instance, self.attr_name, None)
- except AttributeError:
- # We're not on 3.6+ and self.attr_name does not exist
- self.__set_name__(instance, 'dummy')
- val = getattr(instance, self.attr_name, None)
- if val is None:
- raise errors.ObjectDestroyedError()
- return val
-
- def __set__(self, instance, value):
- try:
- setattr(instance, self.attr_name, value)
- except AttributeError:
- # We're not on 3.6+ and self.attr_name does not exist
- self.__set_name__(instance, 'dummy')
- setattr(instance, self.attr_name, value)
-
- def __set_name__(self, instance, name):
- self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
-
-
-class BinString(str):
- """A str subclass with binary data.
-
- Most data in libnotmuch should be valid ASCII or valid UTF-8.
- However since it is a C library these are represented as
- bytestrings intead which means on an API level we can not
- guarantee that decoding this to UTF-8 will both succeed and be
- lossless. This string type converts bytes to unicode in a lossy
- way, but also makes the raw bytes available.
-
- This object is a normal unicode string for most intents and
- purposes, but you can get the original bytestring back by calling
- ``bytes()`` on it.
- """
-
- def __new__(cls, data, encoding='utf-8', errors='ignore'):
- if not isinstance(data, bytes):
- data = bytes(data, encoding=encoding)
- strdata = str(data, encoding=encoding, errors=errors)
- inst = super().__new__(cls, strdata)
- inst._bindata = data
- return inst
-
- @classmethod
- def from_cffi(cls, cdata):
- """Create a new string from a CFFI cdata pointer."""
- return cls(capi.ffi.string(cdata))
-
- def __bytes__(self):
- return self._bindata
-
-
-class NotmuchIter(NotmuchObject, collections.abc.Iterator):
- """An iterator for libnotmuch iterators.
-
- It is tempting to use a generator function instead, but this would
- not correctly respect the :class:`NotmuchObject` memory handling
- protocol and in some unsuspecting cornercases cause memory
- trouble. You probably want to sublcass this in order to wrap the
- value returned by :meth:`__next__`.
-
- :param parent: The parent object.
- :type parent: NotmuchObject
- :param iter_p: The CFFI pointer to the C iterator.
- :type iter_p: cffi.cdata
- :param fn_destory: The CFFI notmuch_*_destroy function.
- :param fn_valid: The CFFI notmuch_*_valid function.
- :param fn_get: The CFFI notmuch_*_get function.
- :param fn_next: The CFFI notmuch_*_move_to_next function.
- """
- _iter_p = MemoryPointer()
-
- def __init__(self, parent, iter_p,
- *, fn_destroy, fn_valid, fn_get, fn_next):
- self._parent = parent
- self._iter_p = iter_p
- self._fn_destroy = fn_destroy
- self._fn_valid = fn_valid
- self._fn_get = fn_get
- self._fn_next = fn_next
-
- def __del__(self):
- self._destroy()
-
- @property
- def alive(self):
- if not self._parent.alive:
- return False
- try:
- self._iter_p
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def _destroy(self):
- if self.alive:
- try:
- self._fn_destroy(self._iter_p)
- except errors.ObjectDestroyedError:
- pass
- self._iter_p = None
-
- def __iter__(self):
- """Return the iterator itself.
-
- Note that as this is an iterator and not a container this will
- not return a new iterator. Thus any elements already consumed
- will not be yielded by the :meth:`__next__` method anymore.
- """
- return self
-
- def __next__(self):
- if not self._fn_valid(self._iter_p):
- self._destroy()
- raise StopIteration()
- obj_p = self._fn_get(self._iter_p)
- self._fn_next(self._iter_p)
- return obj_p
-
- def __repr__(self):
- try:
- self._iter_p
- except errors.ObjectDestroyedError:
- return '<NotmuchIter (exhausted)>'
- else:
- return '<NotmuchIter>'
+++ /dev/null
-import cffi
-
-
-ffibuilder = cffi.FFI()
-ffibuilder.set_source(
- 'notdb._capi',
- r"""
- #include <stdlib.h>
- #include <time.h>
- #include <notmuch.h>
-
- #if LIBNOTMUCH_MAJOR_VERSION < 5
- #error libnotmuch version not supported by notdb
- #endif
- """,
- include_dirs=['../../lib'],
- library_dirs=['../../lib'],
- libraries=['notmuch'],
-)
-ffibuilder.cdef(
- r"""
- void free(void *ptr);
- typedef int... time_t;
-
- #define LIBNOTMUCH_MAJOR_VERSION ...
- #define LIBNOTMUCH_MINOR_VERSION ...
- #define LIBNOTMUCH_MICRO_VERSION ...
-
- #define NOTMUCH_TAG_MAX ...
-
- typedef enum _notmuch_status {
- NOTMUCH_STATUS_SUCCESS = 0,
- NOTMUCH_STATUS_OUT_OF_MEMORY,
- NOTMUCH_STATUS_READ_ONLY_DATABASE,
- NOTMUCH_STATUS_XAPIAN_EXCEPTION,
- NOTMUCH_STATUS_FILE_ERROR,
- NOTMUCH_STATUS_FILE_NOT_EMAIL,
- NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
- NOTMUCH_STATUS_NULL_POINTER,
- NOTMUCH_STATUS_TAG_TOO_LONG,
- NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
- NOTMUCH_STATUS_UNBALANCED_ATOMIC,
- NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
- NOTMUCH_STATUS_UPGRADE_REQUIRED,
- NOTMUCH_STATUS_PATH_ERROR,
- NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
- NOTMUCH_STATUS_LAST_STATUS
- } notmuch_status_t;
- typedef enum {
- NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
- NOTMUCH_DATABASE_MODE_READ_WRITE
- } notmuch_database_mode_t;
- typedef int notmuch_bool_t;
- typedef enum _notmuch_message_flag {
- NOTMUCH_MESSAGE_FLAG_MATCH,
- NOTMUCH_MESSAGE_FLAG_EXCLUDED,
- NOTMUCH_MESSAGE_FLAG_GHOST,
- } notmuch_message_flag_t;
- typedef enum {
- NOTMUCH_SORT_OLDEST_FIRST,
- NOTMUCH_SORT_NEWEST_FIRST,
- NOTMUCH_SORT_MESSAGE_ID,
- NOTMUCH_SORT_UNSORTED
- } notmuch_sort_t;
- typedef enum {
- NOTMUCH_EXCLUDE_FLAG,
- NOTMUCH_EXCLUDE_TRUE,
- NOTMUCH_EXCLUDE_FALSE,
- NOTMUCH_EXCLUDE_ALL
- } notmuch_exclude_t;
-
- // These are fully opaque types for us, we only ever use pointers.
- typedef struct _notmuch_database notmuch_database_t;
- typedef struct _notmuch_query notmuch_query_t;
- typedef struct _notmuch_threads notmuch_threads_t;
- typedef struct _notmuch_thread notmuch_thread_t;
- typedef struct _notmuch_messages notmuch_messages_t;
- typedef struct _notmuch_message notmuch_message_t;
- typedef struct _notmuch_tags notmuch_tags_t;
- typedef struct _notmuch_string_map_iterator notmuch_message_properties_t;
- typedef struct _notmuch_directory notmuch_directory_t;
- typedef struct _notmuch_filenames notmuch_filenames_t;
- typedef struct _notmuch_config_list notmuch_config_list_t;
-
- const char *
- notmuch_status_to_string (notmuch_status_t status);
-
- notmuch_status_t
- notmuch_database_create_verbose (const char *path,
- notmuch_database_t **database,
- char **error_message);
- notmuch_status_t
- notmuch_database_create (const char *path, notmuch_database_t **database);
- notmuch_status_t
- notmuch_database_open_verbose (const char *path,
- notmuch_database_mode_t mode,
- notmuch_database_t **database,
- char **error_message);
- notmuch_status_t
- notmuch_database_open (const char *path,
- notmuch_database_mode_t mode,
- notmuch_database_t **database);
- notmuch_status_t
- notmuch_database_close (notmuch_database_t *database);
- notmuch_status_t
- notmuch_database_destroy (notmuch_database_t *database);
- const char *
- notmuch_database_get_path (notmuch_database_t *database);
- unsigned int
- notmuch_database_get_version (notmuch_database_t *database);
- notmuch_bool_t
- notmuch_database_needs_upgrade (notmuch_database_t *database);
- notmuch_status_t
- notmuch_database_begin_atomic (notmuch_database_t *notmuch);
- notmuch_status_t
- notmuch_database_end_atomic (notmuch_database_t *notmuch);
- unsigned long
- notmuch_database_get_revision (notmuch_database_t *notmuch,
- const char **uuid);
- notmuch_status_t
- notmuch_database_add_message (notmuch_database_t *database,
- const char *filename,
- notmuch_message_t **message);
- notmuch_status_t
- notmuch_database_remove_message (notmuch_database_t *database,
- const char *filename);
- notmuch_status_t
- notmuch_database_find_message (notmuch_database_t *database,
- const char *message_id,
- notmuch_message_t **message);
- notmuch_status_t
- notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
- const char *filename,
- notmuch_message_t **message);
- notmuch_tags_t *
- notmuch_database_get_all_tags (notmuch_database_t *db);
-
- notmuch_query_t *
- notmuch_query_create (notmuch_database_t *database,
- const char *query_string);
- const char *
- notmuch_query_get_query_string (const notmuch_query_t *query);
- notmuch_database_t *
- notmuch_query_get_database (const notmuch_query_t *query);
- void
- notmuch_query_set_omit_excluded (notmuch_query_t *query,
- notmuch_exclude_t omit_excluded);
- void
- notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
- notmuch_sort_t
- notmuch_query_get_sort (const notmuch_query_t *query);
- notmuch_status_t
- notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
- notmuch_status_t
- notmuch_query_search_threads (notmuch_query_t *query,
- notmuch_threads_t **out);
- notmuch_status_t
- notmuch_query_search_messages (notmuch_query_t *query,
- notmuch_messages_t **out);
- notmuch_status_t
- notmuch_query_count_messages (notmuch_query_t *query, unsigned int *count);
- notmuch_status_t
- notmuch_query_count_threads (notmuch_query_t *query, unsigned *count);
- void
- notmuch_query_destroy (notmuch_query_t *query);
-
- notmuch_bool_t
- notmuch_threads_valid (notmuch_threads_t *threads);
- notmuch_thread_t *
- notmuch_threads_get (notmuch_threads_t *threads);
- void
- notmuch_threads_move_to_next (notmuch_threads_t *threads);
- void
- notmuch_threads_destroy (notmuch_threads_t *threads);
-
- const char *
- notmuch_thread_get_thread_id (notmuch_thread_t *thread);
- notmuch_messages_t *
- notmuch_message_get_replies (notmuch_message_t *message);
- int
- notmuch_thread_get_total_messages (notmuch_thread_t *thread);
- notmuch_messages_t *
- notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
- notmuch_messages_t *
- notmuch_thread_get_messages (notmuch_thread_t *thread);
- int
- notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
- const char *
- notmuch_thread_get_authors (notmuch_thread_t *thread);
- const char *
- notmuch_thread_get_subject (notmuch_thread_t *thread);
- time_t
- notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
- time_t
- notmuch_thread_get_newest_date (notmuch_thread_t *thread);
- notmuch_tags_t *
- notmuch_thread_get_tags (notmuch_thread_t *thread);
- void
- notmuch_thread_destroy (notmuch_thread_t *thread);
-
- notmuch_bool_t
- notmuch_messages_valid (notmuch_messages_t *messages);
- notmuch_message_t *
- notmuch_messages_get (notmuch_messages_t *messages);
- void
- notmuch_messages_move_to_next (notmuch_messages_t *messages);
- void
- notmuch_messages_destroy (notmuch_messages_t *messages);
- notmuch_tags_t *
- notmuch_messages_collect_tags (notmuch_messages_t *messages);
-
- const char *
- notmuch_message_get_message_id (notmuch_message_t *message);
- const char *
- notmuch_message_get_thread_id (notmuch_message_t *message);
- const char *
- notmuch_message_get_filename (notmuch_message_t *message);
- notmuch_filenames_t *
- notmuch_message_get_filenames (notmuch_message_t *message);
- notmuch_bool_t
- notmuch_message_get_flag (notmuch_message_t *message,
- notmuch_message_flag_t flag);
- void
- notmuch_message_set_flag (notmuch_message_t *message,
- notmuch_message_flag_t flag,
- notmuch_bool_t value);
- time_t
- notmuch_message_get_date (notmuch_message_t *message);
- const char *
- notmuch_message_get_header (notmuch_message_t *message,
- const char *header);
- notmuch_tags_t *
- notmuch_message_get_tags (notmuch_message_t *message);
- notmuch_status_t
- notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
- notmuch_status_t
- notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
- notmuch_status_t
- notmuch_message_remove_all_tags (notmuch_message_t *message);
- notmuch_status_t
- notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
- notmuch_status_t
- notmuch_message_tags_to_maildir_flags (notmuch_message_t *message);
- notmuch_status_t
- notmuch_message_freeze (notmuch_message_t *message);
- notmuch_status_t
- notmuch_message_thaw (notmuch_message_t *message);
- notmuch_status_t
- notmuch_message_get_property (notmuch_message_t *message,
- const char *key, const char **value);
- notmuch_status_t
- notmuch_message_add_property (notmuch_message_t *message,
- const char *key, const char *value);
- notmuch_status_t
- notmuch_message_remove_property (notmuch_message_t *message,
- const char *key, const char *value);
- notmuch_status_t
- notmuch_message_remove_all_properties (notmuch_message_t *message,
- const char *key);
- notmuch_message_properties_t *
- notmuch_message_get_properties (notmuch_message_t *message,
- const char *key, notmuch_bool_t exact);
- notmuch_bool_t
- notmuch_message_properties_valid (notmuch_message_properties_t
- *properties);
- void
- notmuch_message_properties_move_to_next (notmuch_message_properties_t
- *properties);
- const char *
- notmuch_message_properties_key (notmuch_message_properties_t *properties);
- const char *
- notmuch_message_properties_value (notmuch_message_properties_t
- *properties);
- void
- notmuch_message_properties_destroy (notmuch_message_properties_t
- *properties);
- void
- notmuch_message_destroy (notmuch_message_t *message);
-
- notmuch_bool_t
- notmuch_tags_valid (notmuch_tags_t *tags);
- const char *
- notmuch_tags_get (notmuch_tags_t *tags);
- void
- notmuch_tags_move_to_next (notmuch_tags_t *tags);
- void
- notmuch_tags_destroy (notmuch_tags_t *tags);
-
- notmuch_bool_t
- notmuch_filenames_valid (notmuch_filenames_t *filenames);
- const char *
- notmuch_filenames_get (notmuch_filenames_t *filenames);
- void
- notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
- void
- notmuch_filenames_destroy (notmuch_filenames_t *filenames);
- """
-)
-
-
-if __name__ == '__main__':
- ffibuilder.compile(verbose=True)
+++ /dev/null
-import collections
-import configparser
-import enum
-import functools
-import os
-import pathlib
-import weakref
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-import notdb._message as message
-import notdb._query as querymod
-import notdb._tags as tags
-
-
-__all__ = ['Database', 'AtomicContext', 'DbRevision']
-
-
-def _config_pathname():
- """Return the path of the configuration file.
-
- :rtype: pathlib.Path
- """
- cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
- return pathlib.Path(os.path.expanduser(cfgfname))
-
-
-class Mode(enum.Enum):
- READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
- READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
-
-
-class QuerySortOrder(enum.Enum):
- OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
- NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
- MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
- UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
-
-
-class QueryExclude(enum.Enum):
- TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
- FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
- FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
- ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
-
-
-class Database(base.NotmuchObject):
- """Toplevel access to notmuch.
-
- A :class:`Database` can be opened read-only or read-write.
- Modifications are not atomic by default, use :meth:`begin_atomic`
- for atomic updates. If the underlying database has been modified
- outside of this class a :exc:`XapianError` will be raised and the
- instance must be closed and a new one created.
-
- You can use an instance of this class as a context-manager.
-
- :cvar MODE: The mode a database can be opened with, an enumeration
- of ``READ_ONLY`` and ``READ_WRITE``
- :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
- ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
- :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
- ``FLAG``, ``FALSE`` or ``ALL``. See the query documentation
- for details.
- :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
- :meth:`add` as return value.
- :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
- This is used to implement the ``ro`` and ``rw`` string
- variants.
-
- :ivar closed: Boolean indicating if the database is closed or
- still open.
-
- :param path: The directory of where the database is stored. If
- ``None`` the location will be read from the user's
- configuration file, respecting the ``NOTMUCH_CONFIG``
- environment variable if set.
- :type path: str, bytes, os.PathLike or pathlib.Path
- :param mode: The mode to open the database in. One of
- :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`. For
- convenience you can also use the strings ``ro`` for
- :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
- :type mode: :attr:`MODE` or str.
-
- :raises KeyError: if an unknown mode string is used.
- :raises OSError: or subclasses if the configuration file can not
- be opened.
- :raises configparser.Error: or subclasses if the configuration
- file can not be parsed.
- :raises NotmuchError: or subclasses for other failures.
- """
-
- MODE = Mode
- SORT = QuerySortOrder
- EXCLUDE = QueryExclude
- AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
- _db_p = base.MemoryPointer()
- STR_MODE_MAP = {
- 'ro': MODE.READ_ONLY,
- 'rw': MODE.READ_WRITE,
- }
-
- def __init__(self, path=None, mode=MODE.READ_ONLY):
- if isinstance(mode, str):
- mode = self.STR_MODE_MAP[mode]
- self.mode = mode
- if path is None:
- path = self.default_path()
- if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
- path = bytes(path)
- db_pp = capi.ffi.new('notmuch_database_t **')
- cmsg = capi.ffi.new('char**')
- ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path),
- mode.value, db_pp, cmsg)
- if cmsg[0]:
- msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
- capi.lib.free(cmsg[0])
- else:
- msg = None
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret, msg)
- self._db_p = db_pp[0]
- self.closed = False
-
- @classmethod
- def create(cls, path=None):
- """Create and open database in READ_WRITE mode.
-
- This is creates a new notmuch database and returns an opened
- instance in :attr:`MODE.READ_WRITE` mode.
-
- :param path: The directory of where the database is stored. If
- ``None`` the location will be read from the user's
- configuration file, respecting the ``NOTMUCH_CONFIG``
- environment variable if set.
- :type path: str, bytes or os.PathLike
-
- :raises OSError: or subclasses if the configuration file can not
- be opened.
- :raises configparser.Error: or subclasses if the configuration
- file can not be parsed.
- :raises NotmuchError: if the config file does not have the
- database.path setting.
- :raises FileError: if the database already exists.
-
- :returns: The newly created instance.
- """
- if path is None:
- path = cls.default_path()
- if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
- path = bytes(path)
- db_pp = capi.ffi.new('notmuch_database_t **')
- cmsg = capi.ffi.new('char**')
- ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path),
- db_pp, cmsg)
- if cmsg[0]:
- msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
- capi.lib.free(cmsg[0])
- else:
- msg = None
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret, msg)
-
- # Now close the db and let __init__ open it. Inefficient but
- # creating is not a hot loop while this allows us to have a
- # clean API.
- ret = capi.lib.notmuch_database_destroy(db_pp[0])
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- return cls(path, cls.MODE.READ_WRITE)
-
- @staticmethod
- def default_path(cfg_path=None):
- """Return the path of the user's default database.
-
- This reads the user's configuration file and returns the
- default path of the database.
-
- :param cfg_path: The pathname of the notmuch configuration file.
- If not specified tries to use the pathname provided in the
- :env:`NOTMUCH_CONFIG` environment variable and falls back
- to :file:`~/.notmuch-config.
- :type cfg_path: str, bytes, os.PathLike or pathlib.Path.
-
- :returns: The path of the database, which does not necessarily
- exists.
- :rtype: pathlib.Path
- :raises OSError: or subclasses if the configuration file can not
- be opened.
- :raises configparser.Error: or subclasses if the configuration
- file can not be parsed.
- :raises NotmuchError if the config file does not have the
- database.path setting.
- """
- if not cfg_path:
- cfg_path = _config_pathname()
- if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
- cfg_path = bytes(cfg_path)
- parser = configparser.ConfigParser()
- with open(cfg_path) as fp:
- parser.read_file(fp)
- try:
- return pathlib.Path(parser.get('database', 'path'))
- except configparser.Error:
- raise errors.NotmuchError(
- 'No database.path setting in {}'.format(cfg_path))
-
- def __del__(self):
- self._destroy()
-
- @property
- def alive(self):
- try:
- self._db_p
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def _destroy(self):
- try:
- ret = capi.lib.notmuch_database_destroy(self._db_p)
- except errors.ObjectDestroyedError:
- ret = capi.lib.NOTMUCH_STATUS_SUCCESS
- else:
- self._db_p = None
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def close(self):
- """Close the notmuch database.
-
- Once closed most operations will fail. This can still be
- useful however to explicitly close a database which is opened
- read-write as this would otherwise stop other processes from
- reading the database while it is open.
-
- :raises ObjectDestroyedError: if used after destroyed.
- """
- ret = capi.lib.notmuch_database_close(self._db_p)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- self.closed = True
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- self.close()
-
- @property
- def path(self):
- """The pathname of the notmuch database.
-
- This is returned as a :class:`pathlib.Path` instance.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- try:
- return self._cache_path
- except AttributeError:
- ret = capi.lib.notmuch_database_get_path(self._db_p)
- self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
- return self._cache_path
-
- @property
- def version(self):
- """The database format version.
-
- This is a positive integer.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- try:
- return self._cache_version
- except AttributeError:
- ret = capi.lib.notmuch_database_get_version(self._db_p)
- self._cache_version = ret
- return ret
-
- @property
- def needs_upgrade(self):
- """Whether the database should be upgraded.
-
- If *True* the database can be upgraded using :meth:`upgrade`.
- Not doing so may result in some operations raising
- :exc:`UpgradeRequiredError`.
-
- A read-only database will never be upgradable.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
- return bool(ret)
-
- def upgrade(self, progress_cb=None):
- """Upgrade the database to the latest version.
-
- Upgrade the database, optionally with a progress callback
- which should be a callable which will be called with a
- floating point number in the range of [0.0 .. 1.0].
- """
- raise NotImplementedError
-
- def atomic(self):
- """Return a context manager to perform atomic operations.
-
- The returned context manager can be used to perform atomic
- operations on the database.
-
- .. note:: Unlinke a traditional RDBMS transaction this does
- not imply durability, it only ensures the changes are
- performed atomically.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ctx = AtomicContext(self, '_db_p')
- return ctx
-
- def revision(self):
- """The currently committed revision in the database.
-
- Returned as a ``(revision, uuid)`` namedtuple.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- raw_uuid = capi.ffi.new('char**')
- rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
- return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
-
- def get_directory(self, path):
- raise NotImplementedError
-
- def add(self, filename, *, sync_flags=False):
- """Add a message to the database.
-
- Add a new message to the notmuch database. The message is
- referred to by the pathname of the maildir file. If the
- message ID of the new message already exists in the database,
- this adds ``pathname`` to the list of list of files for the
- existing message.
-
- :param filename: The path of the file containing the message.
- :type filename: str, bytes, os.PathLike or pathlib.Path.
- :param sync_flags: Whether to sync the known maildir flags to
- notmuch tags. See :meth:`Message.flags_to_tags` for
- details.
-
- :returns: A tuple where the first item is the newly inserted
- messages as a :class:`Message` instance, and the second
- item is a boolean indicating if the message inserted was a
- duplicate. This is the namedtuple ``AddedMessage(msg,
- dup)``.
- :rtype: Database.AddedMessage
-
- If an exception is raised, no message was added.
-
- :raises XapianError: A Xapian exception occurred.
- :raises FileError: The file referred to by ``pathname`` could
- not be opened.
- :raises FileNotEmailError: The file referreed to by
- ``pathname`` is not recognised as an email message.
- :raises ReadOnlyDatabaseError: The database is opened in
- READ_ONLY mode.
- :raises UpgradeRequiredError: The database must be upgraded
- first.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
- filename = bytes(filename)
- msg_pp = capi.ffi.new('notmuch_message_t **')
- ret = capi.lib.notmuch_database_add_message(self._db_p,
- os.fsencode(filename),
- msg_pp)
- ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
- capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
- if ret not in ok:
- raise errors.NotmuchError(ret)
- msg = message.Message(self, msg_pp[0], db=self)
- if sync_flags:
- msg.tags.from_maildir_flags()
- return self.AddedMessage(
- msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
-
- def remove(self, filename):
- """Remove a message from the notmuch database.
-
- Removing a message which is not in the database is just a
- silent nop-operation.
-
- :param filename: The pathname of the file containing the
- message to be removed.
- :type filename: str, bytes, os.PathLike or pathlib.Path.
-
- :returns: True if the message is still in the database. This
- can happen when multiple files contain the same message ID.
- The true/false distinction is fairly arbitrary, but think
- of it as ``dup = db.remove_message(name); if dup: ...``.
- :rtype: bool
-
- :raises XapianError: A Xapian exception ocurred.
- :raises ReadOnlyDatabaseError: The database is opened in
- READ_ONLY mode.
- :raises UpgradeRequiredError: The database must be upgraded
- first.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
- filename = bytes(filename)
- ret = capi.lib.notmuch_database_remove_message(self._db_p,
- os.fsencode(filename))
- ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
- capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
- if ret not in ok:
- raise errors.NotmuchError(ret)
- if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
- return True
- else:
- return False
-
- def find(self, msgid):
- """Return the message matching the given message ID.
-
- If a message with the given message ID is found a
- :class:`Message` instance is returned. Otherwise a
- :exc:`LookupError` is raised.
-
- :param msgid: The message ID to look for.
- :type msgid: str
-
- :returns: The message instance.
- :rtype: Message
-
- :raises LookupError: If no message was found.
- :raises OutOfMemoryError: When there is no memory to allocate
- the message instance.
- :raises XapianError: A Xapian exception ocurred.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- msg_pp = capi.ffi.new('notmuch_message_t **')
- ret = capi.lib.notmuch_database_find_message(self._db_p,
- msgid.encode(), msg_pp)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- msg_p = msg_pp[0]
- if msg_p == capi.ffi.NULL:
- raise LookupError
- msg = message.Message(self, msg_p, db=self)
- return msg
-
- def get(self, filename):
- """Return the :class:`Message` given a pathname.
-
- If a message with the given pathname exists in the database
- return the :class:`Message` instance for the message.
- Otherwise raise a :exc:`LookupError` exception.
-
- :param filename: The pathname of the message.
- :type filename: str, bytes, os.PathLike or pathlib.Path
-
- :returns: The message instance.
- :rtype: Message
-
- :raises LookupError: If no message was found. This is also
- a subclass of :exc:`KeyError`.
- :raises OutOfMemoryError: When there is no memory to allocate
- the message instance.
- :raises XapianError: A Xapian exception ocurred.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
- filename = bytes(filename)
- msg_pp = capi.ffi.new('notmuch_message_t **')
- ret = capi.lib.notmuch_database_find_message_by_filename(
- self._db_p, os.fsencode(filename), msg_pp)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- msg_p = msg_pp[0]
- if msg_p == capi.ffi.NULL:
- raise LookupError
- msg = message.Message(self, msg_p, db=self)
- return msg
-
- @property
- def tags(self):
- """Return an immutable set with all tags used in this database.
-
- This returns an immutable set-like object implementing the
- collections.abc.Set Abstract Base Class. Due to the
- underlying libnotmuch implementation some operations have
- different performance characteristics then plain set objects.
- Mainly any lookup operation is O(n) rather then O(1).
-
- Normal usage treats tags as UTF-8 encoded unicode strings so
- they are exposed to Python as normal unicode string objects.
- If you need to handle tags stored in libnotmuch which are not
- valid unicode do check the :class:`ImmutableTagSet` docs for
- how to handle this.
-
- :rtype: ImmutableTagSet
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- try:
- ref = self._cached_tagset
- except AttributeError:
- tagset = None
- else:
- tagset = ref()
- if tagset is None:
- tagset = tags.ImmutableTagSet(
- self, '_db_p', capi.lib.notmuch_database_get_all_tags)
- self._cached_tagset = weakref.ref(tagset)
- return tagset
-
- def _create_query(self, query, *,
- omit_excluded=EXCLUDE.TRUE,
- sort=SORT.UNSORTED, # Check this default
- exclude_tags=None):
- """Create an internal query object.
-
- :raises OutOfMemoryError: if no memory is available to
- allocate the query.
- """
- if isinstance(query, str):
- query = query.encode('utf-8')
- query_p = capi.lib.notmuch_query_create(self._db_p, query)
- if query_p == capi.ffi.NULL:
- raise errors.OutOfMemoryError()
- capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
- capi.lib.notmuch_query_set_sort(query_p, sort.value)
- if exclude_tags is not None:
- for tag in exclude_tags:
- if isinstance(tag, str):
- tag = str.encode('utf-8')
- capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
- return querymod.Query(self, query_p)
-
- def messages(self, query, *,
- omit_excluded=EXCLUDE.TRUE,
- sort=SORT.UNSORTED, # Check this default
- exclude_tags=None):
- """Search the database for messages.
-
- :returns: An iterator over the messages found.
- :rtype: MessageIter
-
- :raises OutOfMemoryError: if no memory is available to
- allocate the query.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- query = self._create_query(query,
- omit_excluded=omit_excluded,
- sort=sort,
- exclude_tags=exclude_tags)
- return query.messages()
-
- def count_messages(self, query, *,
- omit_excluded=EXCLUDE.TRUE,
- sort=SORT.UNSORTED, # Check this default
- exclude_tags=None):
- """Search the database for messages.
-
- :returns: An iterator over the messages found.
- :rtype: MessageIter
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- query = self._create_query(query,
- omit_excluded=omit_excluded,
- sort=sort,
- exclude_tags=exclude_tags)
- return query.count_messages()
-
- def threads(self, query, *,
- omit_excluded=EXCLUDE.TRUE,
- sort=SORT.UNSORTED, # Check this default
- exclude_tags=None):
- query = self._create_query(query,
- omit_excluded=omit_excluded,
- sort=sort,
- exclude_tags=exclude_tags)
- return query.threads()
-
- def count_threads(self, query, *,
- omit_excluded=EXCLUDE.TRUE,
- sort=SORT.UNSORTED, # Check this default
- exclude_tags=None):
- query = self._create_query(query,
- omit_excluded=omit_excluded,
- sort=sort,
- exclude_tags=exclude_tags)
- return query.count_threads()
-
- def status_string(self):
- raise NotImplementedError
-
- def __repr__(self):
- return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
-
-
-class AtomicContext:
- """Context manager for atomic support.
-
- This supports the notmuch_database_begin_atomic and
- notmuch_database_end_atomic API calls. The object can not be
- directly instantiated by the user, only via ``Database.atomic``.
- It does keep a reference to the :class:`Database` instance to keep
- the C memory alive.
-
- :raises XapianError: When this is raised at enter time the atomic
- section is not active. When it is raised at exit time the
- atomic section is still active and you may need to try using
- :meth:`force_end`.
- :raises ObjectDestroyedError: if used after destoryed.
- """
-
- def __init__(self, db, ptr_name):
- self._db = db
- self._ptr = lambda: getattr(db, ptr_name)
-
- def __del__(self):
- self._destroy()
-
- @property
- def alive(self):
- return self.parent.alive
-
- def _destroy(self):
- pass
-
- def __enter__(self):
- ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- ret = capi.lib.notmuch_database_end_atomic(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def force_end(self):
- """Force ending the atomic section.
-
- This can only be called once __exit__ has been called. It
- will attept to close the atomic section (again). This is
- useful if the original exit raised an exception and the atomic
- section is still open. But things are pretty ugly by now.
-
- :raises XapianError: If exiting fails, the atomic section is
- not ended.
- :raises UnbalancedAtomicError: If the database was currently
- not in an atomic section.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_database_end_atomic(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
-
-@functools.total_ordering
-class DbRevision:
- """A database revision.
-
- The database revision number increases monotonically with each
- commit to the database. Which means user-visible changes can be
- ordered. This object is sortable with other revisions. It
- carries the UUID of the database to ensure it is only ever
- compared with revisions from the same database.
- """
-
- def __init__(self, rev, uuid):
- self._rev = rev
- self._uuid = uuid
-
- @property
- def rev(self):
- """The revision number, a positive integer."""
- return self._rev
-
- @property
- def uuid(self):
- """The UUID of the database, consider this opaque."""
- return self._uuid
-
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- if self.uuid != other.uuid:
- return False
- return self.rev == other.rev
- else:
- return NotImplemented
-
- def __lt__(self, other):
- if self.__class__ is other.__class__:
- if self.uuid != other.uuid:
- return False
- return self.rev < other.rev
- else:
- return NotImplemented
-
- def __repr__(self):
- return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
+++ /dev/null
-from notdb import _capi as capi
-
-
-class NotmuchError(Exception):
- """Base exception for errors originating from the notmuch library.
-
- Usually this will have two attributes:
-
- :status: This is a numeric status code corresponding to the error
- code in the notmuch library. This is normally fairly
- meaningless, it can also often be ``None``. This exists mostly
- to easily create new errors from notmuch status codes and
- should not normally be used by users.
-
- :message: A user-facing message for the error. This can
- occasionally also be ``None``. Usually you'll want to call
- ``str()`` on the error object instead to get a sensible
- message.
- """
-
- @classmethod
- def exc_type(cls, status):
- """Return correct exception type for notmuch status."""
- types = {
- capi.lib.NOTMUCH_STATUS_OUT_OF_MEMORY:
- OutOfMemoryError,
- capi.lib.NOTMUCH_STATUS_READ_ONLY_DATABASE:
- ReadOnlyDatabaseError,
- capi.lib.NOTMUCH_STATUS_XAPIAN_EXCEPTION:
- XapianError,
- capi.lib.NOTMUCH_STATUS_FILE_ERROR:
- FileError,
- capi.lib.NOTMUCH_STATUS_FILE_NOT_EMAIL:
- FileNotEmailError,
- capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
- DuplicateMessageIdError,
- capi.lib.NOTMUCH_STATUS_NULL_POINTER:
- NullPointerError,
- capi.lib.NOTMUCH_STATUS_TAG_TOO_LONG:
- TagTooLongError,
- capi.lib.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
- UnbalancedFreezeThawError,
- capi.lib.NOTMUCH_STATUS_UNBALANCED_ATOMIC:
- UnbalancedAtomicError,
- capi.lib.NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
- UnsupportedOperationError,
- capi.lib.NOTMUCH_STATUS_UPGRADE_REQUIRED:
- UpgradeRequiredError,
- capi.lib.NOTMUCH_STATUS_PATH_ERROR:
- PathError,
- capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
- IllegalArgumentError,
- }
- return types[status]
-
- def __new__(cls, *args, **kwargs):
- """Return the correct subclass based on status."""
- # This is simplistic, but the actual __init__ will fail if the
- # signature is wrong anyway.
- if args:
- status = args[0]
- else:
- status = kwargs.get('status', None)
- if status and cls == NotmuchError:
- exc = cls.exc_type(status)
- return exc.__new__(exc, *args, **kwargs)
- else:
- return super().__new__(cls)
-
- def __init__(self, status=None, message=None):
- self.status = status
- self.message = message
-
- def __str__(self):
- if self.message:
- return self.message
- elif self.status:
- return capi.lib.notmuch_status_to_string(self.status)
- else:
- return 'Unknown error'
-
-
-class OutOfMemoryError(NotmuchError): pass
-class ReadOnlyDatabaseError(NotmuchError): pass
-class XapianError(NotmuchError): pass
-class FileError(NotmuchError): pass
-class FileNotEmailError(NotmuchError): pass
-class DuplicateMessageIdError(NotmuchError): pass
-class NullPointerError(NotmuchError): pass
-class TagTooLongError(NotmuchError): pass
-class UnbalancedFreezeThawError(NotmuchError): pass
-class UnbalancedAtomicError(NotmuchError): pass
-class UnsupportedOperationError(NotmuchError): pass
-class UpgradeRequiredError(NotmuchError): pass
-class PathError(NotmuchError): pass
-class IllegalArgumentError(NotmuchError): pass
-
-
-class ObjectDestroyedError(NotmuchError):
- """The object has already been destoryed and it's memory freed.
-
- This occurs when :meth:`destroy` has been called on the object but
- you still happen to have access to the object. This should not
- normally occur since you should never call :meth:`destroy` by
- hand.
- """
-
- def __str__(self):
- if self.message:
- return self.message
- else:
- return 'Memory already freed'
+++ /dev/null
-import collections
-import contextlib
-import os
-import pathlib
-import weakref
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-import notdb._tags as tags
-
-
-__all__ = ['Message']
-
-
-class Message(base.NotmuchObject):
- """An email message stored in the notmuch database.
-
- This should not be directly created, instead it will be returned
- by calling methods on :class:`Database`. A message keeps a
- reference to the database object since the database object can not
- be released while the message is in use.
-
- Note that this represents a message in the notmuch database. For
- full email functionality you may want to use the :mod:`email`
- package from Python's standard library. You could e.g. create
- this as such::
-
- notmuch_msg = db.get_message(msgid) # or from a query
- parser = email.parser.BytesParser(policy=email.policy.default)
- with notmuch_msg.path.open('rb) as fp:
- email_msg = parser.parse(fp)
-
- Most commonly the functionality provided by notmuch is sufficient
- to read email however.
-
- Messages are considered equal when they have the same message ID.
- This is how libnotmuch treats messages as well, the
- :meth:`pathnames` function returns multiple results for
- duplicates.
-
- :param parent: The parent object. This is probably one off a
- :class:`Database`, :class:`Thread` or :class:`Query`.
- :type parent: NotmuchObject
- :param db: The database instance this message is associated with.
- This could be the same as the parent.
- :type db: Database
- :param msg_p: The C pointer to the ``notmuch_message_t``.
- :type msg_p: <cdata>
-
- :param dup: Whether the message was a duplicate on insertion.
-
- :type dup: None or bool
- """
- _msg_p = base.MemoryPointer()
-
- def __init__(self, parent, msg_p, *, db):
- self._parent = parent
- self._msg_p = msg_p
- self._db = db
-
- @property
- def alive(self):
- if not self._parent.alive:
- return False
- try:
- self._msg_p
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def __del__(self):
- self._destroy()
-
- def _destroy(self):
- if self.alive:
- capi.lib.notmuch_message_destroy(self._msg_p)
- self._msg_p = None
-
- @property
- def messageid(self):
- """The message ID as a string.
-
- The message ID is decoded with the ignore error handler. This
- is fine as long as the message ID is well formed. If it is
- not valid ASCII then this will be lossy. So if you need to be
- able to write the exact same message ID back you should use
- :attr:`messageidb`.
-
- Note that notmuch will decode the message ID value and thus
- strip off the surrounding ``<`` and ``>`` characters. This is
- different from Python's :mod:`email` package behaviour which
- leaves these characters in place.
-
- :returns: The message ID.
- :rtype: :class:`BinString`, this is a normal str but calling
- bytes() on it will return the original bytes used to create
- it.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
- return base.BinString(capi.ffi.string(ret))
-
- @property
- def threadid(self):
- """The thread ID.
-
- The thread ID is decoded with the surrogateescape error
- handler so that it is possible to reconstruct the original
- thread ID if it is not valid UTF-8.
-
- :returns: The thread ID.
- :rtype: :class:`BinString`, this is a normal str but calling
- bytes() on it will return the original bytes used to create
- it.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
- return base.BinString(capi.ffi.string(ret))
-
- @property
- def path(self):
- """A pathname of the message as a pathlib.Path instance.
-
- If multiple files in the database contain the same message ID
- this will be just one of the files, chosen at random.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_message_get_filename(self._msg_p)
- return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
-
- @property
- def pathb(self):
- """A pathname of the message as a bytes object.
-
- See :attr:`path` for details, this is the same but does return
- the path as a bytes object which is faster but less convenient.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_message_get_filename(self._msg_p)
- return capi.ffi.string(ret)
-
- def filenames(self):
- """Return an iterator of all files for this message.
-
- If multiple files contained the same message ID they will all
- be returned here. The files are returned as intances of
- :class:`pathlib.Path`.
-
- :returns: Iterator yielding :class:`pathlib.Path` instances.
- :rtype: iter
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
- return PathIter(self, fnames_p)
-
- def filenamesb(self):
- """Return an iterator of all files for this message.
-
- This is like :meth:`pathnames` but the files are returned as
- byte objects instead.
-
- :returns: Iterator yielding :class:`bytes` instances.
- :rtype: iter
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
- return FilenamesIter(self, fnames_p)
-
- @property
- def ghost(self):
- """Indicates whether this message is a ghost message.
-
- A ghost message if a message which we know exists, but it has
- no files or content associated with it. This can happen if
- it was referenced by some other message. Only the
- :attr:`messageid` and :attr:`threadid` attributes are valid
- for it.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_message_get_flag(
- self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
- return bool(ret)
-
- @property
- def excluded(self):
- """Indicates whether this message was excluded from the query.
-
- When a message is created from a search, sometimes messages
- that where excluded by the search query could still be
- returned by it, e.g. because they are part of a thread
- matching the query. the :meth:`Database.query` method allows
- these messages to be flagged, which results in this property
- being set to *True*.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_message_get_flag(
- self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
- return bool(ret)
-
- @property
- def date(self):
- """The message date as an integer.
-
- The time the message was sent as an integer number of seconds
- since the *epoch*, 1 Jan 1970. This is derived from the
- message's header, you can get the original header value with
- :meth:`header`.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- return capi.lib.notmuch_message_get_date(self._msg_p)
-
- def header(self, name):
- """Return the value of the named header.
-
- Returns the header from notmuch, some common headers are
- stored in the database, others are read from the file.
- Headers are returned with their newlines stripped and
- collapsed concatenated together if they occur multiple times.
- You may be better off using the standard library email
- package's ``email.message_from_file(msg.path.open())`` if that
- is not sufficient for you.
-
- :param header: Case-insensitive header name to retrieve.
- :type header: str or bytes
-
- :returns: The header value, an empty string if the header is
- not present.
- :rtype: str
-
- :raises LookupError: if the header is not present.
- :raises NullPointerError: For unexpected notmuch errors.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- # The returned is supposedly guaranteed to be UTF-8. Header
- # names must be ASCII as per RFC x822.
- if isinstance(name, str):
- name = name.encode('ascii')
- ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
- if ret == capi.ffi.NULL:
- raise errors.NullPointerError()
- hdr = capi.ffi.string(ret)
- if not hdr:
- raise LookupError
- return hdr.decode(encoding='utf-8')
-
- @property
- def tags(self):
- """The tags associated with the message.
-
- This behaves as a set. But removing and adding items to the
- set removes and adds them to the message in the database.
-
- :raises ReadOnlyDatabaseError: When manipulating tags on a
- database opened in read-only mode.
- :raises ObjectDestroyedError: if used after destoryed.
- """
- try:
- ref = self._cached_tagset
- except AttributeError:
- tagset = None
- else:
- tagset = ref()
- if tagset is None:
- tagset = tags.MutableTagSet(
- self, '_msg_p', capi.lib.notmuch_message_get_tags)
- self._cached_tagset = weakref.ref(tagset)
- return tagset
-
- @contextlib.contextmanager
- def frozen(self):
- """Context manager to freeze the message state.
-
- This allows you to perform atomic tag updates::
-
- with msg.frozen():
- msg.tags.clear()
- msg.tags.add('foo')
-
- Using This would ensure the message never ends up with no tags
- applied at all.
-
- It is safe to nest calls to this context manager.
-
- :raises ReadOnlyDatabaseError: if the database is opened in
- read-only mode.
- :raises UnbalancedFreezeThawError: if you somehow managed to
- call __exit__ of this context manager more than once. Why
- did you do that?
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_message_freeze(self._msg_p)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- self._frozen = True
- try:
- yield
- except Exception:
- # Only way to "rollback" these changes is to destroy
- # ourselves and re-create. Behold.
- msgid = self.messageid
- self._destroy()
- with contextlib.suppress(Exception):
- new = self._db.find(msgid)
- self._msg_p = new._msg_p
- new._msg_p = None
- del new
- raise
- else:
- ret = capi.lib.notmuch_message_thaw(self._msg_p)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- self._frozen = False
-
- @property
- def properties(self):
- """A map of arbitrary key-value pairs associated with the message.
-
- Be aware that properties may be used by other extensions to
- store state in. So delete or modify with care.
-
- The properties map is somewhat special. It is essentially a
- multimap-like structure where each key can have multiple
- values. Therefore accessing a single item using
- :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
- will only return you the *first* item if there are multiple
- and thus are only recommended if you know there to be only one
- value.
-
- Instead the map has an additional :meth:`PropertiesMap.all`
- method which can be used to retrieve all properties of a given
- key. This method also allows iterating of a a subset of the
- keys starting with a given prefix.
- """
- try:
- ref = self._cached_props
- except AttributeError:
- props = None
- else:
- props = ref()
- if props is None:
- props = PropertiesMap(self, '_msg_p')
- self._cached_props = weakref.ref(props)
- return props
-
- def replies(self):
- """Return an iterator of all replies to this message.
-
- This method will only work if the message was created from a
- thread. Otherwise it will yield no results.
-
- :returns: An iterator yielding :class:`Message` instances.
- :rtype: MessageIter
- """
- # The notmuch_messages_valid call accepts NULL and this will
- # become an empty iterator, raising StopIteration immediately.
- # Hence no return value checking here.
- msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
- return MessageIter(self, msgs_p, db=self._db)
-
- def __hash__(self):
- return hash(self.messageid)
-
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- return self.messageid == other.messageid
-
-
-class FilenamesIter(base.NotmuchIter):
- """Iterator for binary filenames objects."""
-
- def __init__(self, parent, iter_p):
- super().__init__(parent, iter_p,
- fn_destroy=capi.lib.notmuch_filenames_destroy,
- fn_valid=capi.lib.notmuch_filenames_valid,
- fn_get=capi.lib.notmuch_filenames_get,
- fn_next=capi.lib.notmuch_filenames_move_to_next)
-
- def __next__(self):
- fname = super().__next__()
- return capi.ffi.string(fname)
-
-
-class PathIter(FilenamesIter):
- """Iterator for pathlib.Path objects."""
-
- def __next__(self):
- fname = super().__next__()
- return pathlib.Path(os.fsdecode(fname))
-
-
-class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
- """A mutable mapping to manage properties.
-
- Both keys and values of properties are supposed to be UTF-8
- strings in libnotmuch. However since the uderlying API uses
- bytestrings you can use either str or bytes to represent keys and
- all returned keys and values use :class:`BinString`.
-
- Also be aware that ``iter(this_map)`` will return duplicate keys,
- while the :class:`collections.abc.KeysView` returned by
- :meth:`keys` is a :class:`collections.abc.Set` subclass. This
- means the former will yield duplicate keys while the latter won't.
- It also means ``len(list(iter(this_map)))`` could be different
- than ``len(this_map.keys())``. ``len(this_map)`` will correspond
- with the lenght of the default iterator.
-
- Be aware that libnotmuch exposes all of this as iterators, so
- quite a few operations have O(n) performance instead of the usual
- O(1).
- """
- Property = collections.namedtuple('Property', ['key', 'value'])
- _marker = object()
-
- def __init__(self, msg, ptr_name):
- self._msg = msg
- self._ptr = lambda: getattr(msg, ptr_name)
-
- @property
- def alive(self):
- if not self._msg.alive:
- return False
- try:
- self._ptr
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def _destroy(self):
- pass
-
- def __iter__(self):
- """Return an iterator which iterates over the keys.
-
- Be aware that a single key may have multiple values associated
- with it, if so it will appear multiple times here.
- """
- iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
- return PropertiesKeyIter(self, iter_p)
-
- def __len__(self):
- iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
- it = base.NotmuchIter(
- self, iter_p,
- fn_destroy=capi.lib.notmuch_message_properties_destroy,
- fn_valid=capi.lib.notmuch_message_properties_valid,
- fn_get=capi.lib.notmuch_message_properties_key,
- fn_next=capi.lib.notmuch_message_properties_move_to_next,
- )
- return len(list(it))
-
- def __getitem__(self, key):
- """Return **the first** peroperty associated with a key."""
- if isinstance(key, str):
- key = key.encode('utf-8')
- value_pp = capi.ffi.new('char**')
- ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- if value_pp[0] == capi.ffi.NULL:
- raise KeyError
- return base.BinString.from_cffi(value_pp[0])
-
- def keys(self):
- """Return a :class:`collections.abc.KeysView` for this map.
-
- Even when keys occur multiple times this is a subset of set()
- so will only contain them once.
- """
- return collections.abc.KeysView({k: None for k in self})
-
- def items(self):
- """Return a :class:`collections.abc.ItemsView` for this map.
-
- The ItemsView treats a ``(key, value)`` pair as unique, so
- dupcliate ``(key, value)`` pairs will be merged together.
- However duplicate keys with different values will be returned.
- """
- items = set()
- props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
- while capi.lib.notmuch_message_properties_valid(props_p):
- key = capi.lib.notmuch_message_properties_key(props_p)
- value = capi.lib.notmuch_message_properties_value(props_p)
- items.add((base.BinString.from_cffi(key),
- base.BinString.from_cffi(value)))
- capi.lib.notmuch_message_properties_move_to_next(props_p)
- capi.lib.notmuch_message_properties_destroy(props_p)
- return PropertiesItemsView(items)
-
- def values(self):
- """Return a :class:`collecions.abc.ValuesView` for this map.
-
- All unique property values are included in the view.
- """
- values = set()
- props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
- while capi.lib.notmuch_message_properties_valid(props_p):
- value = capi.lib.notmuch_message_properties_value(props_p)
- values.add(base.BinString.from_cffi(value))
- capi.lib.notmuch_message_properties_move_to_next(props_p)
- capi.lib.notmuch_message_properties_destroy(props_p)
- return PropertiesValuesView(values)
-
- def __setitem__(self, key, value):
- """Add a key-value pair to the properties.
-
- You may prefer to use :meth:`add` for clarity since this
- method usually implies implicit overwriting of an existing key
- if it exists, while for properties this is not the case.
- """
- self.add(key, value)
-
- def add(self, key, value):
- """Add a key-value pair to the properties."""
- if isinstance(key, str):
- key = key.encode('utf-8')
- if isinstance(value, str):
- value = value.encode('utf-8')
- ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def __delitem__(self, key):
- """Remove all properties with this key."""
- if isinstance(key, str):
- key = key.encode('utf-8')
- ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def remove(self, key, value):
- """Remove a key-value pair from the properties."""
- if isinstance(key, str):
- key = key.encode('utf-8')
- if isinstance(value, str):
- value = value.encode('utf-8')
- ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def pop(self, key, default=_marker):
- try:
- value = self[key]
- except KeyError:
- if default is self._marker:
- raise
- else:
- return default
- else:
- self.remove(key, value)
- return value
-
- def popitem(self):
- try:
- key = next(iter(self))
- except StopIteration:
- raise KeyError
- value = self.pop(key)
- return (key, value)
-
- def clear(self):
- ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
- capi.ffi.NULL)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def getall(self, prefix='', *, exact=False):
- """Return an iterator yielding all properties for a given key prefix.
-
- The returned iterator yields all peroperties which start with
- a given key prefix as ``(key, value)`` namedtuples. If called
- with ``exact=True`` then only properties which exactly match
- the prefix are returned, those a key longer than the prefix
- will not be included.
-
- :param prefix: The prefix of the key.
- """
- if isinstance(prefix, str):
- prefix = prefix.encode('utf-8')
- props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
- prefix, exact)
- return PropertiesIter(self, props_p)
-
-
-class PropertiesKeyIter(base.NotmuchIter):
-
- def __init__(self, parent, iter_p):
- super().__init__(
- parent,
- iter_p,
- fn_destroy=capi.lib.notmuch_message_properties_destroy,
- fn_valid=capi.lib.notmuch_message_properties_valid,
- fn_get=capi.lib.notmuch_message_properties_key,
- fn_next=capi.lib.notmuch_message_properties_move_to_next)
-
- def __next__(self):
- item = super().__next__()
- return base.BinString.from_cffi(item)
-
-
-class PropertiesIter(base.NotmuchIter):
-
- def __init__(self, parent, iter_p):
- super().__init__(
- parent,
- iter_p,
- fn_destroy=capi.lib.notmuch_message_properties_destroy,
- fn_valid=capi.lib.notmuch_message_properties_valid,
- fn_get=capi.lib.notmuch_message_properties_key,
- fn_next=capi.lib.notmuch_message_properties_move_to_next,
- )
-
- def __next__(self):
- if not self._fn_valid(self._iter_p):
- self._destroy()
- raise StopIteration
- key = capi.lib.notmuch_message_properties_key(self._iter_p)
- value = capi.lib.notmuch_message_properties_value(self._iter_p)
- capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
- return PropertiesMap.Property(base.BinString.from_cffi(key),
- base.BinString.from_cffi(value))
-
-
-class PropertiesItemsView(collections.abc.Set):
-
- __slots__ = ('_items',)
-
- def __init__(self, items):
- self._items = items
-
- @classmethod
- def _from_iterable(self, it):
- return set(it)
-
- def __len__(self):
- return len(self._items)
-
- def __contains__(self, item):
- return item in self._items
-
- def __iter__(self):
- yield from self._items
-
-
-collections.abc.ItemsView.register(PropertiesItemsView)
-
-
-class PropertiesValuesView(collections.abc.Set):
-
- __slots__ = ('_values',)
-
- def __init__(self, values):
- self._values = values
-
- def __len__(self):
- return len(self._values)
-
- def __contains__(self, value):
- return value in self._values
-
- def __iter__(self):
- yield from self._values
-
-
-collections.abc.ValuesView.register(PropertiesValuesView)
-
-
-class MessageIter(base.NotmuchIter):
-
- def __init__(self, parent, msgs_p, *, db):
- self._db = db
- super().__init__(parent, msgs_p,
- fn_destroy=capi.lib.notmuch_messages_destroy,
- fn_valid=capi.lib.notmuch_messages_valid,
- fn_get=capi.lib.notmuch_messages_get,
- fn_next=capi.lib.notmuch_messages_move_to_next)
-
- def __next__(self):
- msg_p = super().__next__()
- return Message(self, msg_p, db=self._db)
+++ /dev/null
-from notdb import _base as base
-from notdb import _capi as capi
-from notdb import _errors as errors
-from notdb import _message as message
-from notdb import _thread as thread
-
-
-__all__ = []
-
-
-class Query(base.NotmuchObject):
- """Private, minimal query object.
-
- This is not meant for users and is not a full implementation of
- the query API. It is only an intermediate used internally to
- match libnotmuch's memory management.
- """
- _query_p = base.MemoryPointer()
-
- def __init__(self, db, query_p):
- self._db = db
- self._query_p = query_p
-
- @property
- def alive(self):
- if not self._db.alive:
- return False
- try:
- self._query_p
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def __del__(self):
- self._destroy()
-
- def _destroy(self):
- if self.alive:
- capi.lib.notmuch_query_destroy(self._query_p)
- self._query_p = None
-
- @property
- def query(self):
- """The query string as seen by libnotmuch."""
- q = capi.lib.notmuch_query_get_query_string(self._query_p)
- return base.BinString.from_cffi(q)
-
- def messages(self):
- """Return an iterator over all the messages found by the query.
-
- This executes the query and returns an iterator over the
- :class:`Message` objects found.
- """
- msgs_pp = capi.ffi.new('notmuch_messages_t**')
- ret = capi.lib.notmuch_query_search_messages(self._query_p, msgs_pp)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- return message.MessageIter(self, msgs_pp[0], db=self._db)
-
- def count_messages(self):
- """Return the number of messages matching this query."""
- count_p = capi.ffi.new('unsigned int *')
- ret = capi.lib.notmuch_query_count_messages(self._query_p, count_p)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- return count_p[0]
-
- def threads(self):
- """Return an iterator over all the threads found by the query."""
- threads_pp = capi.ffi.new('notmuch_threads_t **')
- ret = capi.lib.notmuch_query_search_threads(self._query_p, threads_pp)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- return thread.ThreadIter(self, threads_pp[0], db=self._db)
-
- def count_threads(self):
- """Return the number of threads matching this query."""
- count_p = capi.ffi.new('unsigned int *')
- ret = capi.lib.notmuch_query_count_threads(self._query_p, count_p)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
- return count_p[0]
+++ /dev/null
-import collections.abc
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-
-
-__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
-
-
-class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
- """The tags associated with a message thread or whole database.
-
- Both a thread as well as the database expose the union of all tags
- in messages associated with them. This exposes these as a
- :class:`collections.abc.Set` object.
-
- Note that due to the underlying notmuch API the performance of the
- implementation is not the same as you would expect from normal
- sets. E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
- rather then O(1).
-
- Tags are internally stored as bytestrings but normally exposed as
- unicode strings using the UTF-8 encoding and the *ignore* decoder
- error handler. However the :meth:`iter` method can be used to
- return tags as bytestrings or using a different error handler.
-
- Note that when doing arithmetic operations on tags, this class
- will return a plain normal set as it is no longer associated with
- the message.
-
- :param parent: the parent object
- :param ptr_name: the name of the attribute on the parent which will
- return the memory pointer. This allows this object to
- access the pointer via the parent's descriptor and thus
- trigger :class:`MemoryPointer`'s memory safety.
- :param cffi_fn: the callable CFFI wrapper to retrieve the tags
- iter. This can be one of notmuch_database_get_all_tags,
- notmuch_thread_get_tags or notmuch_message_get_tags.
- """
-
- def __init__(self, parent, ptr_name, cffi_fn):
- self._parent = parent
- self._ptr = lambda: getattr(parent, ptr_name)
- self._cffi_fn = cffi_fn
-
- def __del__(self):
- self._destroy()
-
- @property
- def alive(self):
- return self._parent.alive
-
- def _destroy(self):
- pass
-
- @classmethod
- def _from_iterable(cls, it):
- return set(it)
-
- def __iter__(self):
- """Return an iterator over the tags.
-
- Tags are yielded as unicode strings, decoded using the
- "ignore" error handler.
-
- :raises NullPointerError: If the iterator can not be created.
- """
- return self.iter(encoding='utf-8', errors='ignore')
-
- def iter(self, *, encoding=None, errors='strict'):
- """Aternate iterator constructor controlling string decoding.
-
- Tags are stored as bytes in the notmuch database, in Python
- it's easier to work with unicode strings and thus is what the
- normal iterator returns. However this method allows you to
- specify how you would like to get the tags, defaulting to the
- bytestring representation instead of unicode strings.
-
- :param encoding: Which codec to use. The default *None* does not
- decode at all and will return the unmodified bytes.
- Otherwise this is passed on to :func:`str.decode`.
- :param errors: If using a codec, this is the error handler.
- See :func:`str.decode` to which this is passed on.
-
- :raises NullPointerError: When things do not go as planned.
- """
- # self._cffi_fn should point either to
- # notmuch_database_get_all_tags, notmuch_thread_get_tags or
- # notmuch_message_get_tags. nothmuch.h suggests these never
- # fail, let's handle NULL anyway.
- tags_p = self._cffi_fn(self._ptr())
- if tags_p == capi.ffi.NULL:
- raise errors.NullPointerError()
- tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
- return tags
-
- def __len__(self):
- return sum(1 for t in self)
-
- def __contains__(self, tag):
- if isinstance(tag, str):
- tag = tag.encode()
- for msg_tag in self.iter():
- if tag == msg_tag:
- return True
- else:
- return False
-
- def __eq__(self, other):
- return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
-
- def __hash__(self):
- return hash(tuple(self.iter()))
-
- def __repr__(self):
- return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
- name=self.__class__.__name__,
- addr=id(self),
- tags=', '.join(repr(t) for t in self))
-
-
-class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
- """The tags associated with a message.
-
- This is a :class:`collections.abc.MutableSet` object which can be
- used to manipulate the tags of a message.
-
- Note that due to the underlying notmuch API the performance of the
- implementation is not the same as you would expect from normal
- sets. E.g. the ``in`` operator and variants are O(n) rather then
- O(1).
-
- Tags are bytestrings and calling ``iter()`` will return an
- iterator yielding bytestrings. However the :meth:`iter` method
- can be used to return tags as unicode strings, while all other
- operations accept either byestrings or unicode strings. In case
- unicode strings are used they will be encoded using utf-8 before
- being passed to notmuch.
- """
-
- # Since we subclass ImmutableTagSet we inherit a __hash__. But we
- # are mutable, setting it to None will make the Python machinary
- # recognise us as unhashable.
- __hash__ = None
-
- def add(self, tag):
- """Add a tag to the message.
-
- :param tag: The tag to add.
- :type tag: str or bytes. A str will be encoded using UTF-8.
-
- :param sync_flags: Whether to sync the maildir flags with the
- new set of tags. Leaving this as *None* respects the
- configuration set in the database, while *True* will always
- sync and *False* will never sync.
- :param sync_flags: NoneType or bool
-
- :raises TypeError: If the tag is not a valid type.
- :raises TagTooLongError: If the added tag exceeds the maximum
- lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
- :raises ReadOnlyDatabaseError: If the database is opened in
- read-only mode.
- """
- if isinstance(tag, str):
- tag = tag.encode()
- if not isinstance(tag, bytes):
- raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
- ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def discard(self, tag):
- """Remove a tag from the message.
-
- :param tag: The tag to remove.
- :type tag: str of bytes. A str will be encoded using UTF-8.
- :param sync_flags: Whether to sync the maildir flags with the
- new set of tags. Leaving this as *None* respects the
- configuration set in the database, while *True* will always
- sync and *False* will never sync.
- :param sync_flags: NoneType or bool
-
- :raises TypeError: If the tag is not a valid type.
- :raises TagTooLongError: If the tag exceeds the maximum
- lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
- :raises ReadOnlyDatabaseError: If the database is opened in
- read-only mode.
- """
- if isinstance(tag, str):
- tag = tag.encode()
- if not isinstance(tag, bytes):
- raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
- ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def clear(self):
- """Remove all tags from the message.
-
- :raises ReadOnlyDatabaseError: If the database is opened in
- read-only mode.
- """
- ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def from_maildir_flags(self):
- """Update the tags based on the state in the message's maildir flags.
-
- This function examines the filenames of 'message' for maildir
- flags, and adds or removes tags on 'message' as follows when
- these flags are present:
-
- Flag Action if present
- ---- -----------------
- 'D' Adds the "draft" tag to the message
- 'F' Adds the "flagged" tag to the message
- 'P' Adds the "passed" tag to the message
- 'R' Adds the "replied" tag to the message
- 'S' Removes the "unread" tag from the message
-
- For each flag that is not present, the opposite action
- (add/remove) is performed for the corresponding tags.
-
- Flags are identified as trailing components of the filename
- after a sequence of ":2,".
-
- If there are multiple filenames associated with this message,
- the flag is considered present if it appears in one or more
- filenames. (That is, the flags from the multiple filenames are
- combined with the logical OR operator.)
- """
- ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def to_maildir_flags(self):
- """Update the message's maildir flags based on the notmuch tags.
-
- If the message's filename is in a maildir directory, that is a
- directory named ``new`` or ``cur``, and has a valid maildir
- filename then the flags will be added as such:
-
- 'D' if the message has the "draft" tag
- 'F' if the message has the "flagged" tag
- 'P' if the message has the "passed" tag
- 'R' if the message has the "replied" tag
- 'S' if the message does not have the "unread" tag
-
- Any existing flags unmentioned in the list above will be
- preserved in the renaming.
-
- Also, if this filename is in a directory named "new", rename it to
- be within the neighboring directory named "cur".
-
- In case there are multiple files associated with the message
- all filenames will get the same logic applied.
- """
- ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
-
-class TagsIter(base.NotmuchObject, collections.abc.Iterator):
- """Iterator over tags.
-
- This is only an interator, not a container so calling
- :meth:`__iter__` does not return a new, replenished iterator but
- only itself.
-
- :param parent: The parent object to keep alive.
- :param tags_p: The CFFI pointer to the C-level tags iterator.
- :param encoding: Which codec to use. The default *None* does not
- decode at all and will return the unmodified bytes.
- Otherwise this is passed on to :func:`str.decode`.
- :param errors: If using a codec, this is the error handler.
- See :func:`str.decode` to which this is passed on.
-
- :raises ObjectDestoryedError: if used after destroyed.
- """
- _tags_p = base.MemoryPointer()
-
- def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
- self._parent = parent
- self._tags_p = tags_p
- self._encoding = encoding
- self._errors = errors
-
- def __del__(self):
- self._destroy()
-
- @property
- def alive(self):
- if not self._parent.alive:
- return False
- try:
- self._tags_p
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def _destroy(self):
- if self.alive:
- try:
- capi.lib.notmuch_tags_destroy(self._tags_p)
- except errors.ObjectDestroyedError:
- pass
- self._tags_p = None
-
- def __iter__(self):
- """Return the iterator itself.
-
- Note that as this is an iterator and not a container this will
- not return a new iterator. Thus any elements already consumed
- will not be yielded by the :meth:`__next__` method anymore.
- """
- return self
-
- def __next__(self):
- if not capi.lib.notmuch_tags_valid(self._tags_p):
- self._destroy()
- raise StopIteration()
- tag_p = capi.lib.notmuch_tags_get(self._tags_p)
- tag = capi.ffi.string(tag_p)
- if self._encoding:
- tag = tag.decode(encoding=self._encoding, errors=self._errors)
- capi.lib.notmuch_tags_move_to_next(self._tags_p)
- return tag
-
- def __repr__(self):
- try:
- self._tags_p
- except errors.ObjectDestroyedError:
- return '<TagsIter (exhausted)>'
- else:
- return '<TagsIter>'
+++ /dev/null
-import collections.abc
-import weakref
-
-from notdb import _base as base
-from notdb import _capi as capi
-from notdb import _errors as errors
-from notdb import _message as message
-from notdb import _tags as tags
-
-
-__all__ = ['Thread']
-
-
-class Thread(base.NotmuchObject, collections.abc.Iterable):
- _thread_p = base.MemoryPointer()
-
- def __init__(self, parent, thread_p, *, db):
- self._parent = parent
- self._thread_p = thread_p
- self._db = db
-
- @property
- def alive(self):
- if not self._parent.alive:
- return False
- try:
- self._thread_p
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def __del__(self):
- self._destroy()
-
- def _destroy(self):
- if self.alive:
- capi.lib.notmuch_thread_destroy(self._thread_p)
- self._thread_p = None
-
- @property
- def threadid(self):
- """The thread ID as a :class:`BinString`.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_thread_get_thread_id(self._thread_p)
- return base.BinString.from_cffi(ret)
-
- def __len__(self):
- """Return the number of messages in the thread.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- return capi.lib.notmuch_thread_get_total_messages(self._thread_p)
-
- def toplevel(self):
- """Return an iterator of the toplevel messages.
-
- :returns: An iterator yielding :class:`Message` instances.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- msgs_p = capi.lib.notmuch_thread_get_toplevel_messages(self._thread_p)
- return message.MessageIter(self, msgs_p, db=self._db)
-
- def __iter__(self):
- """Return an iterator over all the messages in the thread.
-
- :returns: An iterator yielding :class:`Message` instances.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- msgs_p = capi.lib.notmuch_thread_get_messages(self._thread_p)
- return message.MessageIter(self, msgs_p, db=self._db)
-
- @property
- def matched(self):
- """The number of messages in this thread which matched the query.
-
- Of the messages in the thread this gives the count of messages
- which did directly match the search query which this thread
- originates from.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- return capi.lib.notmuch_thread_get_matched_messages(self._thread_p)
-
- @property
- def authors(self):
- """A comma-separated string of all authors in the thread.
-
- Authors of messages which matched the query the thread was
- retrieved from will be at the head of the string, ordered by
- date of their messages. Following this will be the authors of
- the other messages in the thread, also ordered by date of
- their messages. Both groups of authors are separated by the
- ``|`` character.
-
- :returns: The stringified list of authors.
- :rtype: BinString
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_thread_get_authors(self._thread_p)
- return base.BinString.from_cffi(ret)
-
- @property
- def subject(self):
- """The subject of the thread, taken from the first message.
-
- The thread's subject is taken to be the subject of the first
- message according to query sort order.
-
- :returns: The thread's subject.
- :rtype: BinString
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- ret = capi.lib.notmuch_thread_get_subject(self._thread_p)
- return base.BinString.from_cffi(ret)
-
- @property
- def first(self):
- """Return the date of the oldest message in the thread.
-
- The time the first message was sent as an integer number of
- seconds since the *epoch*, 1 Jan 1970.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- return capi.lib.notmuch_thread_get_oldest_date(self._thread_p)
-
- @property
- def last(self):
- """Return the date of the newest message in the thread.
-
- The time the last message was sent as an integer number of
- seconds since the *epoch*, 1 Jan 1970.
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- return capi.lib.notmuch_thread_get_newest_date(self._thread_p)
-
- @property
- def tags(self):
- """Return an immutable set with all tags used in this thread.
-
- This returns an immutable set-like object implementing the
- collections.abc.Set Abstract Base Class. Due to the
- underlying libnotmuch implementation some operations have
- different performance characteristics then plain set objects.
- Mainly any lookup operation is O(n) rather then O(1).
-
- Normal usage treats tags as UTF-8 encoded unicode strings so
- they are exposed to Python as normal unicode string objects.
- If you need to handle tags stored in libnotmuch which are not
- valid unicode do check the :class:`ImmutableTagSet` docs for
- how to handle this.
-
- :rtype: ImmutableTagSet
-
- :raises ObjectDestroyedError: if used after destoryed.
- """
- try:
- ref = self._cached_tagset
- except AttributeError:
- tagset = None
- else:
- tagset = ref()
- if tagset is None:
- tagset = tags.ImmutableTagSet(
- self, '_thread_p', capi.lib.notmuch_thread_get_tags)
- self._cached_tagset = weakref.ref(tagset)
- return tagset
-
-
-class ThreadIter(base.NotmuchIter):
-
- def __init__(self, parent, threads_p, *, db):
- self._db = db
- super().__init__(parent, threads_p,
- fn_destroy=capi.lib.notmuch_threads_destroy,
- fn_valid=capi.lib.notmuch_threads_valid,
- fn_get=capi.lib.notmuch_threads_get,
- fn_next=capi.lib.notmuch_threads_move_to_next)
-
- def __next__(self):
- thread_p = super().__next__()
- return Thread(self, thread_p, db=self._db)
--- /dev/null
+"""Pythonic API to the notmuch database.
+
+Creating Objects
+================
+
+Only the :class:`Database` object is meant to be created by the user.
+All other objects should be created from this initial object. Users
+should consider their signatures implementation details.
+
+Errors
+======
+
+All errors occuring due to errors from the underlying notmuch database
+are subclasses of the :exc:`NotmuchError`. Due to memory management
+it is possible to try and use an object after it has been freed. In
+this case a :exc:`ObjectDestoryedError` will be raised.
+
+Memory Management
+=================
+
+Libnotmuch uses a hierarchical memory allocator, this means all
+objects have a strict parent-child relationship and when the parent is
+freed all the children are freed as well. This has some implications
+for these Python bindings as parent objects need to be kept alive.
+This is normally schielded entirely from the user however and the
+Python objects automatically make sure the right references are kept
+alive. It is however the reason the :class:`BaseObject` exists as it
+defines the API all Python objects need to implement to work
+correctly.
+
+Collections and Containers
+==========================
+
+Libnotmuch exposes nearly all collections of things as iterators only.
+In these python bindings they have sometimes been exposed as
+:class:`collections.abc.Container` instances or subclasses of this
+like :class:`collections.abc.Set` or :class:`collections.abc.Mapping`
+etc. This gives a more natural API to work with, e.g. being able to
+treat tags as sets. However it does mean that the
+:meth:`__contains__`, :meth:`__len__` and frieds methods on these are
+usually more and essentially O(n) rather than O(1) as you might
+usually expect from Python containers.
+"""
+
+from notmuch2 import _capi
+from notmuch2._base import *
+from notmuch2._database import *
+from notmuch2._errors import *
+from notmuch2._message import *
+from notmuch2._tags import *
+from notmuch2._thread import *
+
+
+NOTMUCH_TAG_MAX = _capi.lib.NOTMUCH_TAG_MAX
+del _capi
+
+
+# Re-home all the objects to the package. This leaves __qualname__ intact.
+for x in locals().copy().values():
+ if hasattr(x, '__module__'):
+ x.__module__ = __name__
+del x
--- /dev/null
+import abc
+import collections.abc
+
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+
+
+__all__ = ['NotmuchObject', 'BinString']
+
+
+class NotmuchObject(metaclass=abc.ABCMeta):
+ """Base notmuch object syntax.
+
+ This base class exists to define the memory management handling
+ required to use the notmuch library. It is meant as an interface
+ definition rather than a base class, though you can use it as a
+ base class to ensure you don't forget part of the interface. It
+ only concerns you if you are implementing this package itself
+ rather then using it.
+
+ libnotmuch uses a hierarchical memory allocator, where freeing the
+ memory of a parent object also frees the memory of all child
+ objects. To make this work seamlessly in Python this package
+ keeps references to parent objects which makes them stay alive
+ correctly under normal circumstances. When an object finally gets
+ deleted the :meth:`__del__` method will be called to free the
+ memory.
+
+ However during some peculiar situations, e.g. interpreter
+ shutdown, it is possible for the :meth:`__del__` method to have
+ been called, whele there are still references to an object. This
+ could result in child objects asking their memeory to be freed
+ after the parent has already freed the memory, making things
+ rather unhappy as double frees are not taken lightly in C. To
+ handle this case all objects need to follow the same protocol to
+ destroy themselves, see :meth:`destroy`.
+
+ Once an object has been destroyed trying to use it should raise
+ the :exc:`ObjectDestroyedError` exception. For this see also the
+ convenience :class:`MemoryPointer` descriptor in this module which
+ can be used as a pointer to libnotmuch memory.
+ """
+
+ @abc.abstractmethod
+ def __init__(self, parent, *args, **kwargs):
+ """Create a new object.
+
+ Other then for the toplevel :class:`Database` object
+ constructors are only ever called by internal code and not by
+ the user. Per convention their signature always takes the
+ parent object as first argument. Feel free to make the rest
+ of the signature match the object's requirement. The object
+ needs to keep a reference to the parent, so it can check the
+ parent is still alive.
+ """
+
+ @property
+ @abc.abstractmethod
+ def alive(self):
+ """Whether the object is still alive.
+
+ This indicates whether the object is still alive. The first
+ thing this needs to check is whether the parent object is
+ still alive, if it is not then this object can not be alive
+ either. If the parent is alive then it depends on whether the
+ memory for this object has been freed yet or not.
+ """
+
+ def __del__(self):
+ self._destroy()
+
+ @abc.abstractmethod
+ def _destroy(self):
+ """Destroy the object, freeing all memory.
+
+ This method needs to destory the object on the
+ libnotmuch-level. It must ensure it's not been destroyed by
+ it's parent object yet before doing so. It also must be
+ idempotent.
+ """
+
+
+class MemoryPointer:
+ """Data Descriptor to handle accessing libnotmuch pointers.
+
+ Most :class:`NotmuchObject` instances will have one or more CFFI
+ pointers to C-objects. Once an object is destroyed this pointer
+ should no longer be used and a :exc:`ObjectDestroyedError`
+ exception should be raised on trying to access it. This
+ descriptor simplifies implementing this, allowing the creation of
+ an attribute which can be assigned to, but when accessed when the
+ stored value is *None* it will raise the
+ :exc:`ObjectDestroyedError` exception::
+
+ class SomeOjb:
+ _ptr = MemoryPointer()
+
+ def __init__(self, ptr):
+ self._ptr = ptr
+
+ def destroy(self):
+ somehow_free(self._ptr)
+ self._ptr = None
+
+ def do_something(self):
+ return some_libnotmuch_call(self._ptr)
+ """
+
+ def __get__(self, instance, owner):
+ try:
+ val = getattr(instance, self.attr_name, None)
+ except AttributeError:
+ # We're not on 3.6+ and self.attr_name does not exist
+ self.__set_name__(instance, 'dummy')
+ val = getattr(instance, self.attr_name, None)
+ if val is None:
+ raise errors.ObjectDestroyedError()
+ return val
+
+ def __set__(self, instance, value):
+ try:
+ setattr(instance, self.attr_name, value)
+ except AttributeError:
+ # We're not on 3.6+ and self.attr_name does not exist
+ self.__set_name__(instance, 'dummy')
+ setattr(instance, self.attr_name, value)
+
+ def __set_name__(self, instance, name):
+ self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
+
+
+class BinString(str):
+ """A str subclass with binary data.
+
+ Most data in libnotmuch should be valid ASCII or valid UTF-8.
+ However since it is a C library these are represented as
+ bytestrings intead which means on an API level we can not
+ guarantee that decoding this to UTF-8 will both succeed and be
+ lossless. This string type converts bytes to unicode in a lossy
+ way, but also makes the raw bytes available.
+
+ This object is a normal unicode string for most intents and
+ purposes, but you can get the original bytestring back by calling
+ ``bytes()`` on it.
+ """
+
+ def __new__(cls, data, encoding='utf-8', errors='ignore'):
+ if not isinstance(data, bytes):
+ data = bytes(data, encoding=encoding)
+ strdata = str(data, encoding=encoding, errors=errors)
+ inst = super().__new__(cls, strdata)
+ inst._bindata = data
+ return inst
+
+ @classmethod
+ def from_cffi(cls, cdata):
+ """Create a new string from a CFFI cdata pointer."""
+ return cls(capi.ffi.string(cdata))
+
+ def __bytes__(self):
+ return self._bindata
+
+
+class NotmuchIter(NotmuchObject, collections.abc.Iterator):
+ """An iterator for libnotmuch iterators.
+
+ It is tempting to use a generator function instead, but this would
+ not correctly respect the :class:`NotmuchObject` memory handling
+ protocol and in some unsuspecting cornercases cause memory
+ trouble. You probably want to sublcass this in order to wrap the
+ value returned by :meth:`__next__`.
+
+ :param parent: The parent object.
+ :type parent: NotmuchObject
+ :param iter_p: The CFFI pointer to the C iterator.
+ :type iter_p: cffi.cdata
+ :param fn_destory: The CFFI notmuch_*_destroy function.
+ :param fn_valid: The CFFI notmuch_*_valid function.
+ :param fn_get: The CFFI notmuch_*_get function.
+ :param fn_next: The CFFI notmuch_*_move_to_next function.
+ """
+ _iter_p = MemoryPointer()
+
+ def __init__(self, parent, iter_p,
+ *, fn_destroy, fn_valid, fn_get, fn_next):
+ self._parent = parent
+ self._iter_p = iter_p
+ self._fn_destroy = fn_destroy
+ self._fn_valid = fn_valid
+ self._fn_get = fn_get
+ self._fn_next = fn_next
+
+ def __del__(self):
+ self._destroy()
+
+ @property
+ def alive(self):
+ if not self._parent.alive:
+ return False
+ try:
+ self._iter_p
+ except errors.ObjectDestroyedError:
+ return False
+ else:
+ return True
+
+ def _destroy(self):
+ if self.alive:
+ try:
+ self._fn_destroy(self._iter_p)
+ except errors.ObjectDestroyedError:
+ pass
+ self._iter_p = None
+
+ def __iter__(self):
+ """Return the iterator itself.
+
+ Note that as this is an iterator and not a container this will
+ not return a new iterator. Thus any elements already consumed
+ will not be yielded by the :meth:`__next__` method anymore.
+ """
+ return self
+
+ def __next__(self):
+ if not self._fn_valid(self._iter_p):
+ self._destroy()
+ raise StopIteration()
+ obj_p = self._fn_get(self._iter_p)
+ self._fn_next(self._iter_p)
+ return obj_p
+
+ def __repr__(self):
+ try:
+ self._iter_p
+ except errors.ObjectDestroyedError:
+ return '<NotmuchIter (exhausted)>'
+ else:
+ return '<NotmuchIter>'
--- /dev/null
+import cffi
+
+
+ffibuilder = cffi.FFI()
+ffibuilder.set_source(
+ 'notmuch2._capi',
+ r"""
+ #include <stdlib.h>
+ #include <time.h>
+ #include <notmuch.h>
+
+ #if LIBNOTMUCH_MAJOR_VERSION < 5
+ #error libnotmuch version not supported by notmuch2 python bindings
+ #endif
+ """,
+ include_dirs=['../../lib'],
+ library_dirs=['../../lib'],
+ libraries=['notmuch'],
+)
+ffibuilder.cdef(
+ r"""
+ void free(void *ptr);
+ typedef int... time_t;
+
+ #define LIBNOTMUCH_MAJOR_VERSION ...
+ #define LIBNOTMUCH_MINOR_VERSION ...
+ #define LIBNOTMUCH_MICRO_VERSION ...
+
+ #define NOTMUCH_TAG_MAX ...
+
+ typedef enum _notmuch_status {
+ NOTMUCH_STATUS_SUCCESS = 0,
+ NOTMUCH_STATUS_OUT_OF_MEMORY,
+ NOTMUCH_STATUS_READ_ONLY_DATABASE,
+ NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+ NOTMUCH_STATUS_FILE_ERROR,
+ NOTMUCH_STATUS_FILE_NOT_EMAIL,
+ NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
+ NOTMUCH_STATUS_NULL_POINTER,
+ NOTMUCH_STATUS_TAG_TOO_LONG,
+ NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+ NOTMUCH_STATUS_UNBALANCED_ATOMIC,
+ NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
+ NOTMUCH_STATUS_UPGRADE_REQUIRED,
+ NOTMUCH_STATUS_PATH_ERROR,
+ NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
+ NOTMUCH_STATUS_LAST_STATUS
+ } notmuch_status_t;
+ typedef enum {
+ NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
+ NOTMUCH_DATABASE_MODE_READ_WRITE
+ } notmuch_database_mode_t;
+ typedef int notmuch_bool_t;
+ typedef enum _notmuch_message_flag {
+ NOTMUCH_MESSAGE_FLAG_MATCH,
+ NOTMUCH_MESSAGE_FLAG_EXCLUDED,
+ NOTMUCH_MESSAGE_FLAG_GHOST,
+ } notmuch_message_flag_t;
+ typedef enum {
+ NOTMUCH_SORT_OLDEST_FIRST,
+ NOTMUCH_SORT_NEWEST_FIRST,
+ NOTMUCH_SORT_MESSAGE_ID,
+ NOTMUCH_SORT_UNSORTED
+ } notmuch_sort_t;
+ typedef enum {
+ NOTMUCH_EXCLUDE_FLAG,
+ NOTMUCH_EXCLUDE_TRUE,
+ NOTMUCH_EXCLUDE_FALSE,
+ NOTMUCH_EXCLUDE_ALL
+ } notmuch_exclude_t;
+
+ // These are fully opaque types for us, we only ever use pointers.
+ typedef struct _notmuch_database notmuch_database_t;
+ typedef struct _notmuch_query notmuch_query_t;
+ typedef struct _notmuch_threads notmuch_threads_t;
+ typedef struct _notmuch_thread notmuch_thread_t;
+ typedef struct _notmuch_messages notmuch_messages_t;
+ typedef struct _notmuch_message notmuch_message_t;
+ typedef struct _notmuch_tags notmuch_tags_t;
+ typedef struct _notmuch_string_map_iterator notmuch_message_properties_t;
+ typedef struct _notmuch_directory notmuch_directory_t;
+ typedef struct _notmuch_filenames notmuch_filenames_t;
+ typedef struct _notmuch_config_list notmuch_config_list_t;
+
+ const char *
+ notmuch_status_to_string (notmuch_status_t status);
+
+ notmuch_status_t
+ notmuch_database_create_verbose (const char *path,
+ notmuch_database_t **database,
+ char **error_message);
+ notmuch_status_t
+ notmuch_database_create (const char *path, notmuch_database_t **database);
+ notmuch_status_t
+ notmuch_database_open_verbose (const char *path,
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database,
+ char **error_message);
+ notmuch_status_t
+ notmuch_database_open (const char *path,
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database);
+ notmuch_status_t
+ notmuch_database_close (notmuch_database_t *database);
+ notmuch_status_t
+ notmuch_database_destroy (notmuch_database_t *database);
+ const char *
+ notmuch_database_get_path (notmuch_database_t *database);
+ unsigned int
+ notmuch_database_get_version (notmuch_database_t *database);
+ notmuch_bool_t
+ notmuch_database_needs_upgrade (notmuch_database_t *database);
+ notmuch_status_t
+ notmuch_database_begin_atomic (notmuch_database_t *notmuch);
+ notmuch_status_t
+ notmuch_database_end_atomic (notmuch_database_t *notmuch);
+ unsigned long
+ notmuch_database_get_revision (notmuch_database_t *notmuch,
+ const char **uuid);
+ notmuch_status_t
+ notmuch_database_add_message (notmuch_database_t *database,
+ const char *filename,
+ notmuch_message_t **message);
+ notmuch_status_t
+ notmuch_database_remove_message (notmuch_database_t *database,
+ const char *filename);
+ notmuch_status_t
+ notmuch_database_find_message (notmuch_database_t *database,
+ const char *message_id,
+ notmuch_message_t **message);
+ notmuch_status_t
+ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
+ const char *filename,
+ notmuch_message_t **message);
+ notmuch_tags_t *
+ notmuch_database_get_all_tags (notmuch_database_t *db);
+
+ notmuch_query_t *
+ notmuch_query_create (notmuch_database_t *database,
+ const char *query_string);
+ const char *
+ notmuch_query_get_query_string (const notmuch_query_t *query);
+ notmuch_database_t *
+ notmuch_query_get_database (const notmuch_query_t *query);
+ void
+ notmuch_query_set_omit_excluded (notmuch_query_t *query,
+ notmuch_exclude_t omit_excluded);
+ void
+ notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
+ notmuch_sort_t
+ notmuch_query_get_sort (const notmuch_query_t *query);
+ notmuch_status_t
+ notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
+ notmuch_status_t
+ notmuch_query_search_threads (notmuch_query_t *query,
+ notmuch_threads_t **out);
+ notmuch_status_t
+ notmuch_query_search_messages (notmuch_query_t *query,
+ notmuch_messages_t **out);
+ notmuch_status_t
+ notmuch_query_count_messages (notmuch_query_t *query, unsigned int *count);
+ notmuch_status_t
+ notmuch_query_count_threads (notmuch_query_t *query, unsigned *count);
+ void
+ notmuch_query_destroy (notmuch_query_t *query);
+
+ notmuch_bool_t
+ notmuch_threads_valid (notmuch_threads_t *threads);
+ notmuch_thread_t *
+ notmuch_threads_get (notmuch_threads_t *threads);
+ void
+ notmuch_threads_move_to_next (notmuch_threads_t *threads);
+ void
+ notmuch_threads_destroy (notmuch_threads_t *threads);
+
+ const char *
+ notmuch_thread_get_thread_id (notmuch_thread_t *thread);
+ notmuch_messages_t *
+ notmuch_message_get_replies (notmuch_message_t *message);
+ int
+ notmuch_thread_get_total_messages (notmuch_thread_t *thread);
+ notmuch_messages_t *
+ notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
+ notmuch_messages_t *
+ notmuch_thread_get_messages (notmuch_thread_t *thread);
+ int
+ notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
+ const char *
+ notmuch_thread_get_authors (notmuch_thread_t *thread);
+ const char *
+ notmuch_thread_get_subject (notmuch_thread_t *thread);
+ time_t
+ notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
+ time_t
+ notmuch_thread_get_newest_date (notmuch_thread_t *thread);
+ notmuch_tags_t *
+ notmuch_thread_get_tags (notmuch_thread_t *thread);
+ void
+ notmuch_thread_destroy (notmuch_thread_t *thread);
+
+ notmuch_bool_t
+ notmuch_messages_valid (notmuch_messages_t *messages);
+ notmuch_message_t *
+ notmuch_messages_get (notmuch_messages_t *messages);
+ void
+ notmuch_messages_move_to_next (notmuch_messages_t *messages);
+ void
+ notmuch_messages_destroy (notmuch_messages_t *messages);
+ notmuch_tags_t *
+ notmuch_messages_collect_tags (notmuch_messages_t *messages);
+
+ const char *
+ notmuch_message_get_message_id (notmuch_message_t *message);
+ const char *
+ notmuch_message_get_thread_id (notmuch_message_t *message);
+ const char *
+ notmuch_message_get_filename (notmuch_message_t *message);
+ notmuch_filenames_t *
+ notmuch_message_get_filenames (notmuch_message_t *message);
+ notmuch_bool_t
+ notmuch_message_get_flag (notmuch_message_t *message,
+ notmuch_message_flag_t flag);
+ void
+ notmuch_message_set_flag (notmuch_message_t *message,
+ notmuch_message_flag_t flag,
+ notmuch_bool_t value);
+ time_t
+ notmuch_message_get_date (notmuch_message_t *message);
+ const char *
+ notmuch_message_get_header (notmuch_message_t *message,
+ const char *header);
+ notmuch_tags_t *
+ notmuch_message_get_tags (notmuch_message_t *message);
+ notmuch_status_t
+ notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
+ notmuch_status_t
+ notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
+ notmuch_status_t
+ notmuch_message_remove_all_tags (notmuch_message_t *message);
+ notmuch_status_t
+ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
+ notmuch_status_t
+ notmuch_message_tags_to_maildir_flags (notmuch_message_t *message);
+ notmuch_status_t
+ notmuch_message_freeze (notmuch_message_t *message);
+ notmuch_status_t
+ notmuch_message_thaw (notmuch_message_t *message);
+ notmuch_status_t
+ notmuch_message_get_property (notmuch_message_t *message,
+ const char *key, const char **value);
+ notmuch_status_t
+ notmuch_message_add_property (notmuch_message_t *message,
+ const char *key, const char *value);
+ notmuch_status_t
+ notmuch_message_remove_property (notmuch_message_t *message,
+ const char *key, const char *value);
+ notmuch_status_t
+ notmuch_message_remove_all_properties (notmuch_message_t *message,
+ const char *key);
+ notmuch_message_properties_t *
+ notmuch_message_get_properties (notmuch_message_t *message,
+ const char *key, notmuch_bool_t exact);
+ notmuch_bool_t
+ notmuch_message_properties_valid (notmuch_message_properties_t
+ *properties);
+ void
+ notmuch_message_properties_move_to_next (notmuch_message_properties_t
+ *properties);
+ const char *
+ notmuch_message_properties_key (notmuch_message_properties_t *properties);
+ const char *
+ notmuch_message_properties_value (notmuch_message_properties_t
+ *properties);
+ void
+ notmuch_message_properties_destroy (notmuch_message_properties_t
+ *properties);
+ void
+ notmuch_message_destroy (notmuch_message_t *message);
+
+ notmuch_bool_t
+ notmuch_tags_valid (notmuch_tags_t *tags);
+ const char *
+ notmuch_tags_get (notmuch_tags_t *tags);
+ void
+ notmuch_tags_move_to_next (notmuch_tags_t *tags);
+ void
+ notmuch_tags_destroy (notmuch_tags_t *tags);
+
+ notmuch_bool_t
+ notmuch_filenames_valid (notmuch_filenames_t *filenames);
+ const char *
+ notmuch_filenames_get (notmuch_filenames_t *filenames);
+ void
+ notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
+ void
+ notmuch_filenames_destroy (notmuch_filenames_t *filenames);
+ """
+)
+
+
+if __name__ == '__main__':
+ ffibuilder.compile(verbose=True)
--- /dev/null
+import collections
+import configparser
+import enum
+import functools
+import os
+import pathlib
+import weakref
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+import notmuch2._message as message
+import notmuch2._query as querymod
+import notmuch2._tags as tags
+
+
+__all__ = ['Database', 'AtomicContext', 'DbRevision']
+
+
+def _config_pathname():
+ """Return the path of the configuration file.
+
+ :rtype: pathlib.Path
+ """
+ cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
+ return pathlib.Path(os.path.expanduser(cfgfname))
+
+
+class Mode(enum.Enum):
+ READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
+ READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
+
+
+class QuerySortOrder(enum.Enum):
+ OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
+ NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
+ MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
+ UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
+
+
+class QueryExclude(enum.Enum):
+ TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
+ FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
+ FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
+ ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
+
+
+class Database(base.NotmuchObject):
+ """Toplevel access to notmuch.
+
+ A :class:`Database` can be opened read-only or read-write.
+ Modifications are not atomic by default, use :meth:`begin_atomic`
+ for atomic updates. If the underlying database has been modified
+ outside of this class a :exc:`XapianError` will be raised and the
+ instance must be closed and a new one created.
+
+ You can use an instance of this class as a context-manager.
+
+ :cvar MODE: The mode a database can be opened with, an enumeration
+ of ``READ_ONLY`` and ``READ_WRITE``
+ :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
+ ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
+ :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
+ ``FLAG``, ``FALSE`` or ``ALL``. See the query documentation
+ for details.
+ :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
+ :meth:`add` as return value.
+ :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
+ This is used to implement the ``ro`` and ``rw`` string
+ variants.
+
+ :ivar closed: Boolean indicating if the database is closed or
+ still open.
+
+ :param path: The directory of where the database is stored. If
+ ``None`` the location will be read from the user's
+ configuration file, respecting the ``NOTMUCH_CONFIG``
+ environment variable if set.
+ :type path: str, bytes, os.PathLike or pathlib.Path
+ :param mode: The mode to open the database in. One of
+ :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`. For
+ convenience you can also use the strings ``ro`` for
+ :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
+ :type mode: :attr:`MODE` or str.
+
+ :raises KeyError: if an unknown mode string is used.
+ :raises OSError: or subclasses if the configuration file can not
+ be opened.
+ :raises configparser.Error: or subclasses if the configuration
+ file can not be parsed.
+ :raises NotmuchError: or subclasses for other failures.
+ """
+
+ MODE = Mode
+ SORT = QuerySortOrder
+ EXCLUDE = QueryExclude
+ AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
+ _db_p = base.MemoryPointer()
+ STR_MODE_MAP = {
+ 'ro': MODE.READ_ONLY,
+ 'rw': MODE.READ_WRITE,
+ }
+
+ def __init__(self, path=None, mode=MODE.READ_ONLY):
+ if isinstance(mode, str):
+ mode = self.STR_MODE_MAP[mode]
+ self.mode = mode
+ if path is None:
+ path = self.default_path()
+ if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
+ path = bytes(path)
+ db_pp = capi.ffi.new('notmuch_database_t **')
+ cmsg = capi.ffi.new('char**')
+ ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path),
+ mode.value, db_pp, cmsg)
+ if cmsg[0]:
+ msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
+ capi.lib.free(cmsg[0])
+ else:
+ msg = None
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret, msg)
+ self._db_p = db_pp[0]
+ self.closed = False
+
+ @classmethod
+ def create(cls, path=None):
+ """Create and open database in READ_WRITE mode.
+
+ This is creates a new notmuch database and returns an opened
+ instance in :attr:`MODE.READ_WRITE` mode.
+
+ :param path: The directory of where the database is stored. If
+ ``None`` the location will be read from the user's
+ configuration file, respecting the ``NOTMUCH_CONFIG``
+ environment variable if set.
+ :type path: str, bytes or os.PathLike
+
+ :raises OSError: or subclasses if the configuration file can not
+ be opened.
+ :raises configparser.Error: or subclasses if the configuration
+ file can not be parsed.
+ :raises NotmuchError: if the config file does not have the
+ database.path setting.
+ :raises FileError: if the database already exists.
+
+ :returns: The newly created instance.
+ """
+ if path is None:
+ path = cls.default_path()
+ if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
+ path = bytes(path)
+ db_pp = capi.ffi.new('notmuch_database_t **')
+ cmsg = capi.ffi.new('char**')
+ ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path),
+ db_pp, cmsg)
+ if cmsg[0]:
+ msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
+ capi.lib.free(cmsg[0])
+ else:
+ msg = None
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret, msg)
+
+ # Now close the db and let __init__ open it. Inefficient but
+ # creating is not a hot loop while this allows us to have a
+ # clean API.
+ ret = capi.lib.notmuch_database_destroy(db_pp[0])
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ return cls(path, cls.MODE.READ_WRITE)
+
+ @staticmethod
+ def default_path(cfg_path=None):
+ """Return the path of the user's default database.
+
+ This reads the user's configuration file and returns the
+ default path of the database.
+
+ :param cfg_path: The pathname of the notmuch configuration file.
+ If not specified tries to use the pathname provided in the
+ :env:`NOTMUCH_CONFIG` environment variable and falls back
+ to :file:`~/.notmuch-config.
+ :type cfg_path: str, bytes, os.PathLike or pathlib.Path.
+
+ :returns: The path of the database, which does not necessarily
+ exists.
+ :rtype: pathlib.Path
+ :raises OSError: or subclasses if the configuration file can not
+ be opened.
+ :raises configparser.Error: or subclasses if the configuration
+ file can not be parsed.
+ :raises NotmuchError if the config file does not have the
+ database.path setting.
+ """
+ if not cfg_path:
+ cfg_path = _config_pathname()
+ if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
+ cfg_path = bytes(cfg_path)
+ parser = configparser.ConfigParser()
+ with open(cfg_path) as fp:
+ parser.read_file(fp)
+ try:
+ return pathlib.Path(parser.get('database', 'path'))
+ except configparser.Error:
+ raise errors.NotmuchError(
+ 'No database.path setting in {}'.format(cfg_path))
+
+ def __del__(self):
+ self._destroy()
+
+ @property
+ def alive(self):
+ try:
+ self._db_p
+ except errors.ObjectDestroyedError:
+ return False
+ else:
+ return True
+
+ def _destroy(self):
+ try:
+ ret = capi.lib.notmuch_database_destroy(self._db_p)
+ except errors.ObjectDestroyedError:
+ ret = capi.lib.NOTMUCH_STATUS_SUCCESS
+ else:
+ self._db_p = None
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def close(self):
+ """Close the notmuch database.
+
+ Once closed most operations will fail. This can still be
+ useful however to explicitly close a database which is opened
+ read-write as this would otherwise stop other processes from
+ reading the database while it is open.
+
+ :raises ObjectDestroyedError: if used after destroyed.
+ """
+ ret = capi.lib.notmuch_database_close(self._db_p)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ self.closed = True
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ @property
+ def path(self):
+ """The pathname of the notmuch database.
+
+ This is returned as a :class:`pathlib.Path` instance.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ try:
+ return self._cache_path
+ except AttributeError:
+ ret = capi.lib.notmuch_database_get_path(self._db_p)
+ self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
+ return self._cache_path
+
+ @property
+ def version(self):
+ """The database format version.
+
+ This is a positive integer.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ try:
+ return self._cache_version
+ except AttributeError:
+ ret = capi.lib.notmuch_database_get_version(self._db_p)
+ self._cache_version = ret
+ return ret
+
+ @property
+ def needs_upgrade(self):
+ """Whether the database should be upgraded.
+
+ If *True* the database can be upgraded using :meth:`upgrade`.
+ Not doing so may result in some operations raising
+ :exc:`UpgradeRequiredError`.
+
+ A read-only database will never be upgradable.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
+ return bool(ret)
+
+ def upgrade(self, progress_cb=None):
+ """Upgrade the database to the latest version.
+
+ Upgrade the database, optionally with a progress callback
+ which should be a callable which will be called with a
+ floating point number in the range of [0.0 .. 1.0].
+ """
+ raise NotImplementedError
+
+ def atomic(self):
+ """Return a context manager to perform atomic operations.
+
+ The returned context manager can be used to perform atomic
+ operations on the database.
+
+ .. note:: Unlinke a traditional RDBMS transaction this does
+ not imply durability, it only ensures the changes are
+ performed atomically.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ctx = AtomicContext(self, '_db_p')
+ return ctx
+
+ def revision(self):
+ """The currently committed revision in the database.
+
+ Returned as a ``(revision, uuid)`` namedtuple.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ raw_uuid = capi.ffi.new('char**')
+ rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
+ return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
+
+ def get_directory(self, path):
+ raise NotImplementedError
+
+ def add(self, filename, *, sync_flags=False):
+ """Add a message to the database.
+
+ Add a new message to the notmuch database. The message is
+ referred to by the pathname of the maildir file. If the
+ message ID of the new message already exists in the database,
+ this adds ``pathname`` to the list of list of files for the
+ existing message.
+
+ :param filename: The path of the file containing the message.
+ :type filename: str, bytes, os.PathLike or pathlib.Path.
+ :param sync_flags: Whether to sync the known maildir flags to
+ notmuch tags. See :meth:`Message.flags_to_tags` for
+ details.
+
+ :returns: A tuple where the first item is the newly inserted
+ messages as a :class:`Message` instance, and the second
+ item is a boolean indicating if the message inserted was a
+ duplicate. This is the namedtuple ``AddedMessage(msg,
+ dup)``.
+ :rtype: Database.AddedMessage
+
+ If an exception is raised, no message was added.
+
+ :raises XapianError: A Xapian exception occurred.
+ :raises FileError: The file referred to by ``pathname`` could
+ not be opened.
+ :raises FileNotEmailError: The file referreed to by
+ ``pathname`` is not recognised as an email message.
+ :raises ReadOnlyDatabaseError: The database is opened in
+ READ_ONLY mode.
+ :raises UpgradeRequiredError: The database must be upgraded
+ first.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+ filename = bytes(filename)
+ msg_pp = capi.ffi.new('notmuch_message_t **')
+ ret = capi.lib.notmuch_database_add_message(self._db_p,
+ os.fsencode(filename),
+ msg_pp)
+ ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
+ capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
+ if ret not in ok:
+ raise errors.NotmuchError(ret)
+ msg = message.Message(self, msg_pp[0], db=self)
+ if sync_flags:
+ msg.tags.from_maildir_flags()
+ return self.AddedMessage(
+ msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
+
+ def remove(self, filename):
+ """Remove a message from the notmuch database.
+
+ Removing a message which is not in the database is just a
+ silent nop-operation.
+
+ :param filename: The pathname of the file containing the
+ message to be removed.
+ :type filename: str, bytes, os.PathLike or pathlib.Path.
+
+ :returns: True if the message is still in the database. This
+ can happen when multiple files contain the same message ID.
+ The true/false distinction is fairly arbitrary, but think
+ of it as ``dup = db.remove_message(name); if dup: ...``.
+ :rtype: bool
+
+ :raises XapianError: A Xapian exception ocurred.
+ :raises ReadOnlyDatabaseError: The database is opened in
+ READ_ONLY mode.
+ :raises UpgradeRequiredError: The database must be upgraded
+ first.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+ filename = bytes(filename)
+ ret = capi.lib.notmuch_database_remove_message(self._db_p,
+ os.fsencode(filename))
+ ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
+ capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
+ if ret not in ok:
+ raise errors.NotmuchError(ret)
+ if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+ return True
+ else:
+ return False
+
+ def find(self, msgid):
+ """Return the message matching the given message ID.
+
+ If a message with the given message ID is found a
+ :class:`Message` instance is returned. Otherwise a
+ :exc:`LookupError` is raised.
+
+ :param msgid: The message ID to look for.
+ :type msgid: str
+
+ :returns: The message instance.
+ :rtype: Message
+
+ :raises LookupError: If no message was found.
+ :raises OutOfMemoryError: When there is no memory to allocate
+ the message instance.
+ :raises XapianError: A Xapian exception ocurred.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ msg_pp = capi.ffi.new('notmuch_message_t **')
+ ret = capi.lib.notmuch_database_find_message(self._db_p,
+ msgid.encode(), msg_pp)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ msg_p = msg_pp[0]
+ if msg_p == capi.ffi.NULL:
+ raise LookupError
+ msg = message.Message(self, msg_p, db=self)
+ return msg
+
+ def get(self, filename):
+ """Return the :class:`Message` given a pathname.
+
+ If a message with the given pathname exists in the database
+ return the :class:`Message` instance for the message.
+ Otherwise raise a :exc:`LookupError` exception.
+
+ :param filename: The pathname of the message.
+ :type filename: str, bytes, os.PathLike or pathlib.Path
+
+ :returns: The message instance.
+ :rtype: Message
+
+ :raises LookupError: If no message was found. This is also
+ a subclass of :exc:`KeyError`.
+ :raises OutOfMemoryError: When there is no memory to allocate
+ the message instance.
+ :raises XapianError: A Xapian exception ocurred.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+ filename = bytes(filename)
+ msg_pp = capi.ffi.new('notmuch_message_t **')
+ ret = capi.lib.notmuch_database_find_message_by_filename(
+ self._db_p, os.fsencode(filename), msg_pp)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ msg_p = msg_pp[0]
+ if msg_p == capi.ffi.NULL:
+ raise LookupError
+ msg = message.Message(self, msg_p, db=self)
+ return msg
+
+ @property
+ def tags(self):
+ """Return an immutable set with all tags used in this database.
+
+ This returns an immutable set-like object implementing the
+ collections.abc.Set Abstract Base Class. Due to the
+ underlying libnotmuch implementation some operations have
+ different performance characteristics then plain set objects.
+ Mainly any lookup operation is O(n) rather then O(1).
+
+ Normal usage treats tags as UTF-8 encoded unicode strings so
+ they are exposed to Python as normal unicode string objects.
+ If you need to handle tags stored in libnotmuch which are not
+ valid unicode do check the :class:`ImmutableTagSet` docs for
+ how to handle this.
+
+ :rtype: ImmutableTagSet
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ try:
+ ref = self._cached_tagset
+ except AttributeError:
+ tagset = None
+ else:
+ tagset = ref()
+ if tagset is None:
+ tagset = tags.ImmutableTagSet(
+ self, '_db_p', capi.lib.notmuch_database_get_all_tags)
+ self._cached_tagset = weakref.ref(tagset)
+ return tagset
+
+ def _create_query(self, query, *,
+ omit_excluded=EXCLUDE.TRUE,
+ sort=SORT.UNSORTED, # Check this default
+ exclude_tags=None):
+ """Create an internal query object.
+
+ :raises OutOfMemoryError: if no memory is available to
+ allocate the query.
+ """
+ if isinstance(query, str):
+ query = query.encode('utf-8')
+ query_p = capi.lib.notmuch_query_create(self._db_p, query)
+ if query_p == capi.ffi.NULL:
+ raise errors.OutOfMemoryError()
+ capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
+ capi.lib.notmuch_query_set_sort(query_p, sort.value)
+ if exclude_tags is not None:
+ for tag in exclude_tags:
+ if isinstance(tag, str):
+ tag = str.encode('utf-8')
+ capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
+ return querymod.Query(self, query_p)
+
+ def messages(self, query, *,
+ omit_excluded=EXCLUDE.TRUE,
+ sort=SORT.UNSORTED, # Check this default
+ exclude_tags=None):
+ """Search the database for messages.
+
+ :returns: An iterator over the messages found.
+ :rtype: MessageIter
+
+ :raises OutOfMemoryError: if no memory is available to
+ allocate the query.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ query = self._create_query(query,
+ omit_excluded=omit_excluded,
+ sort=sort,
+ exclude_tags=exclude_tags)
+ return query.messages()
+
+ def count_messages(self, query, *,
+ omit_excluded=EXCLUDE.TRUE,
+ sort=SORT.UNSORTED, # Check this default
+ exclude_tags=None):
+ """Search the database for messages.
+
+ :returns: An iterator over the messages found.
+ :rtype: MessageIter
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ query = self._create_query(query,
+ omit_excluded=omit_excluded,
+ sort=sort,
+ exclude_tags=exclude_tags)
+ return query.count_messages()
+
+ def threads(self, query, *,
+ omit_excluded=EXCLUDE.TRUE,
+ sort=SORT.UNSORTED, # Check this default
+ exclude_tags=None):
+ query = self._create_query(query,
+ omit_excluded=omit_excluded,
+ sort=sort,
+ exclude_tags=exclude_tags)
+ return query.threads()
+
+ def count_threads(self, query, *,
+ omit_excluded=EXCLUDE.TRUE,
+ sort=SORT.UNSORTED, # Check this default
+ exclude_tags=None):
+ query = self._create_query(query,
+ omit_excluded=omit_excluded,
+ sort=sort,
+ exclude_tags=exclude_tags)
+ return query.count_threads()
+
+ def status_string(self):
+ raise NotImplementedError
+
+ def __repr__(self):
+ return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
+
+
+class AtomicContext:
+ """Context manager for atomic support.
+
+ This supports the notmuch_database_begin_atomic and
+ notmuch_database_end_atomic API calls. The object can not be
+ directly instantiated by the user, only via ``Database.atomic``.
+ It does keep a reference to the :class:`Database` instance to keep
+ the C memory alive.
+
+ :raises XapianError: When this is raised at enter time the atomic
+ section is not active. When it is raised at exit time the
+ atomic section is still active and you may need to try using
+ :meth:`force_end`.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+
+ def __init__(self, db, ptr_name):
+ self._db = db
+ self._ptr = lambda: getattr(db, ptr_name)
+
+ def __del__(self):
+ self._destroy()
+
+ @property
+ def alive(self):
+ return self.parent.alive
+
+ def _destroy(self):
+ pass
+
+ def __enter__(self):
+ ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ ret = capi.lib.notmuch_database_end_atomic(self._ptr())
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def force_end(self):
+ """Force ending the atomic section.
+
+ This can only be called once __exit__ has been called. It
+ will attept to close the atomic section (again). This is
+ useful if the original exit raised an exception and the atomic
+ section is still open. But things are pretty ugly by now.
+
+ :raises XapianError: If exiting fails, the atomic section is
+ not ended.
+ :raises UnbalancedAtomicError: If the database was currently
+ not in an atomic section.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_database_end_atomic(self._ptr())
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+
+@functools.total_ordering
+class DbRevision:
+ """A database revision.
+
+ The database revision number increases monotonically with each
+ commit to the database. Which means user-visible changes can be
+ ordered. This object is sortable with other revisions. It
+ carries the UUID of the database to ensure it is only ever
+ compared with revisions from the same database.
+ """
+
+ def __init__(self, rev, uuid):
+ self._rev = rev
+ self._uuid = uuid
+
+ @property
+ def rev(self):
+ """The revision number, a positive integer."""
+ return self._rev
+
+ @property
+ def uuid(self):
+ """The UUID of the database, consider this opaque."""
+ return self._uuid
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ if self.uuid != other.uuid:
+ return False
+ return self.rev == other.rev
+ else:
+ return NotImplemented
+
+ def __lt__(self, other):
+ if self.__class__ is other.__class__:
+ if self.uuid != other.uuid:
+ return False
+ return self.rev < other.rev
+ else:
+ return NotImplemented
+
+ def __repr__(self):
+ return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
--- /dev/null
+from notmuch2 import _capi as capi
+
+
+class NotmuchError(Exception):
+ """Base exception for errors originating from the notmuch library.
+
+ Usually this will have two attributes:
+
+ :status: This is a numeric status code corresponding to the error
+ code in the notmuch library. This is normally fairly
+ meaningless, it can also often be ``None``. This exists mostly
+ to easily create new errors from notmuch status codes and
+ should not normally be used by users.
+
+ :message: A user-facing message for the error. This can
+ occasionally also be ``None``. Usually you'll want to call
+ ``str()`` on the error object instead to get a sensible
+ message.
+ """
+
+ @classmethod
+ def exc_type(cls, status):
+ """Return correct exception type for notmuch status."""
+ types = {
+ capi.lib.NOTMUCH_STATUS_OUT_OF_MEMORY:
+ OutOfMemoryError,
+ capi.lib.NOTMUCH_STATUS_READ_ONLY_DATABASE:
+ ReadOnlyDatabaseError,
+ capi.lib.NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+ XapianError,
+ capi.lib.NOTMUCH_STATUS_FILE_ERROR:
+ FileError,
+ capi.lib.NOTMUCH_STATUS_FILE_NOT_EMAIL:
+ FileNotEmailError,
+ capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+ DuplicateMessageIdError,
+ capi.lib.NOTMUCH_STATUS_NULL_POINTER:
+ NullPointerError,
+ capi.lib.NOTMUCH_STATUS_TAG_TOO_LONG:
+ TagTooLongError,
+ capi.lib.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+ UnbalancedFreezeThawError,
+ capi.lib.NOTMUCH_STATUS_UNBALANCED_ATOMIC:
+ UnbalancedAtomicError,
+ capi.lib.NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
+ UnsupportedOperationError,
+ capi.lib.NOTMUCH_STATUS_UPGRADE_REQUIRED:
+ UpgradeRequiredError,
+ capi.lib.NOTMUCH_STATUS_PATH_ERROR:
+ PathError,
+ capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
+ IllegalArgumentError,
+ }
+ return types[status]
+
+ def __new__(cls, *args, **kwargs):
+ """Return the correct subclass based on status."""
+ # This is simplistic, but the actual __init__ will fail if the
+ # signature is wrong anyway.
+ if args:
+ status = args[0]
+ else:
+ status = kwargs.get('status', None)
+ if status and cls == NotmuchError:
+ exc = cls.exc_type(status)
+ return exc.__new__(exc, *args, **kwargs)
+ else:
+ return super().__new__(cls)
+
+ def __init__(self, status=None, message=None):
+ self.status = status
+ self.message = message
+
+ def __str__(self):
+ if self.message:
+ return self.message
+ elif self.status:
+ return capi.lib.notmuch_status_to_string(self.status)
+ else:
+ return 'Unknown error'
+
+
+class OutOfMemoryError(NotmuchError): pass
+class ReadOnlyDatabaseError(NotmuchError): pass
+class XapianError(NotmuchError): pass
+class FileError(NotmuchError): pass
+class FileNotEmailError(NotmuchError): pass
+class DuplicateMessageIdError(NotmuchError): pass
+class NullPointerError(NotmuchError): pass
+class TagTooLongError(NotmuchError): pass
+class UnbalancedFreezeThawError(NotmuchError): pass
+class UnbalancedAtomicError(NotmuchError): pass
+class UnsupportedOperationError(NotmuchError): pass
+class UpgradeRequiredError(NotmuchError): pass
+class PathError(NotmuchError): pass
+class IllegalArgumentError(NotmuchError): pass
+
+
+class ObjectDestroyedError(NotmuchError):
+ """The object has already been destoryed and it's memory freed.
+
+ This occurs when :meth:`destroy` has been called on the object but
+ you still happen to have access to the object. This should not
+ normally occur since you should never call :meth:`destroy` by
+ hand.
+ """
+
+ def __str__(self):
+ if self.message:
+ return self.message
+ else:
+ return 'Memory already freed'
--- /dev/null
+import collections
+import contextlib
+import os
+import pathlib
+import weakref
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+import notmuch2._tags as tags
+
+
+__all__ = ['Message']
+
+
+class Message(base.NotmuchObject):
+ """An email message stored in the notmuch database.
+
+ This should not be directly created, instead it will be returned
+ by calling methods on :class:`Database`. A message keeps a
+ reference to the database object since the database object can not
+ be released while the message is in use.
+
+ Note that this represents a message in the notmuch database. For
+ full email functionality you may want to use the :mod:`email`
+ package from Python's standard library. You could e.g. create
+ this as such::
+
+ notmuch_msg = db.get_message(msgid) # or from a query
+ parser = email.parser.BytesParser(policy=email.policy.default)
+ with notmuch_msg.path.open('rb) as fp:
+ email_msg = parser.parse(fp)
+
+ Most commonly the functionality provided by notmuch is sufficient
+ to read email however.
+
+ Messages are considered equal when they have the same message ID.
+ This is how libnotmuch treats messages as well, the
+ :meth:`pathnames` function returns multiple results for
+ duplicates.
+
+ :param parent: The parent object. This is probably one off a
+ :class:`Database`, :class:`Thread` or :class:`Query`.
+ :type parent: NotmuchObject
+ :param db: The database instance this message is associated with.
+ This could be the same as the parent.
+ :type db: Database
+ :param msg_p: The C pointer to the ``notmuch_message_t``.
+ :type msg_p: <cdata>
+
+ :param dup: Whether the message was a duplicate on insertion.
+
+ :type dup: None or bool
+ """
+ _msg_p = base.MemoryPointer()
+
+ def __init__(self, parent, msg_p, *, db):
+ self._parent = parent
+ self._msg_p = msg_p
+ self._db = db
+
+ @property
+ def alive(self):
+ if not self._parent.alive:
+ return False
+ try:
+ self._msg_p
+ except errors.ObjectDestroyedError:
+ return False
+ else:
+ return True
+
+ def __del__(self):
+ self._destroy()
+
+ def _destroy(self):
+ if self.alive:
+ capi.lib.notmuch_message_destroy(self._msg_p)
+ self._msg_p = None
+
+ @property
+ def messageid(self):
+ """The message ID as a string.
+
+ The message ID is decoded with the ignore error handler. This
+ is fine as long as the message ID is well formed. If it is
+ not valid ASCII then this will be lossy. So if you need to be
+ able to write the exact same message ID back you should use
+ :attr:`messageidb`.
+
+ Note that notmuch will decode the message ID value and thus
+ strip off the surrounding ``<`` and ``>`` characters. This is
+ different from Python's :mod:`email` package behaviour which
+ leaves these characters in place.
+
+ :returns: The message ID.
+ :rtype: :class:`BinString`, this is a normal str but calling
+ bytes() on it will return the original bytes used to create
+ it.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
+ return base.BinString(capi.ffi.string(ret))
+
+ @property
+ def threadid(self):
+ """The thread ID.
+
+ The thread ID is decoded with the surrogateescape error
+ handler so that it is possible to reconstruct the original
+ thread ID if it is not valid UTF-8.
+
+ :returns: The thread ID.
+ :rtype: :class:`BinString`, this is a normal str but calling
+ bytes() on it will return the original bytes used to create
+ it.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
+ return base.BinString(capi.ffi.string(ret))
+
+ @property
+ def path(self):
+ """A pathname of the message as a pathlib.Path instance.
+
+ If multiple files in the database contain the same message ID
+ this will be just one of the files, chosen at random.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_message_get_filename(self._msg_p)
+ return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
+
+ @property
+ def pathb(self):
+ """A pathname of the message as a bytes object.
+
+ See :attr:`path` for details, this is the same but does return
+ the path as a bytes object which is faster but less convenient.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_message_get_filename(self._msg_p)
+ return capi.ffi.string(ret)
+
+ def filenames(self):
+ """Return an iterator of all files for this message.
+
+ If multiple files contained the same message ID they will all
+ be returned here. The files are returned as intances of
+ :class:`pathlib.Path`.
+
+ :returns: Iterator yielding :class:`pathlib.Path` instances.
+ :rtype: iter
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
+ return PathIter(self, fnames_p)
+
+ def filenamesb(self):
+ """Return an iterator of all files for this message.
+
+ This is like :meth:`pathnames` but the files are returned as
+ byte objects instead.
+
+ :returns: Iterator yielding :class:`bytes` instances.
+ :rtype: iter
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
+ return FilenamesIter(self, fnames_p)
+
+ @property
+ def ghost(self):
+ """Indicates whether this message is a ghost message.
+
+ A ghost message if a message which we know exists, but it has
+ no files or content associated with it. This can happen if
+ it was referenced by some other message. Only the
+ :attr:`messageid` and :attr:`threadid` attributes are valid
+ for it.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_message_get_flag(
+ self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
+ return bool(ret)
+
+ @property
+ def excluded(self):
+ """Indicates whether this message was excluded from the query.
+
+ When a message is created from a search, sometimes messages
+ that where excluded by the search query could still be
+ returned by it, e.g. because they are part of a thread
+ matching the query. the :meth:`Database.query` method allows
+ these messages to be flagged, which results in this property
+ being set to *True*.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_message_get_flag(
+ self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
+ return bool(ret)
+
+ @property
+ def date(self):
+ """The message date as an integer.
+
+ The time the message was sent as an integer number of seconds
+ since the *epoch*, 1 Jan 1970. This is derived from the
+ message's header, you can get the original header value with
+ :meth:`header`.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ return capi.lib.notmuch_message_get_date(self._msg_p)
+
+ def header(self, name):
+ """Return the value of the named header.
+
+ Returns the header from notmuch, some common headers are
+ stored in the database, others are read from the file.
+ Headers are returned with their newlines stripped and
+ collapsed concatenated together if they occur multiple times.
+ You may be better off using the standard library email
+ package's ``email.message_from_file(msg.path.open())`` if that
+ is not sufficient for you.
+
+ :param header: Case-insensitive header name to retrieve.
+ :type header: str or bytes
+
+ :returns: The header value, an empty string if the header is
+ not present.
+ :rtype: str
+
+ :raises LookupError: if the header is not present.
+ :raises NullPointerError: For unexpected notmuch errors.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ # The returned is supposedly guaranteed to be UTF-8. Header
+ # names must be ASCII as per RFC x822.
+ if isinstance(name, str):
+ name = name.encode('ascii')
+ ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
+ if ret == capi.ffi.NULL:
+ raise errors.NullPointerError()
+ hdr = capi.ffi.string(ret)
+ if not hdr:
+ raise LookupError
+ return hdr.decode(encoding='utf-8')
+
+ @property
+ def tags(self):
+ """The tags associated with the message.
+
+ This behaves as a set. But removing and adding items to the
+ set removes and adds them to the message in the database.
+
+ :raises ReadOnlyDatabaseError: When manipulating tags on a
+ database opened in read-only mode.
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ try:
+ ref = self._cached_tagset
+ except AttributeError:
+ tagset = None
+ else:
+ tagset = ref()
+ if tagset is None:
+ tagset = tags.MutableTagSet(
+ self, '_msg_p', capi.lib.notmuch_message_get_tags)
+ self._cached_tagset = weakref.ref(tagset)
+ return tagset
+
+ @contextlib.contextmanager
+ def frozen(self):
+ """Context manager to freeze the message state.
+
+ This allows you to perform atomic tag updates::
+
+ with msg.frozen():
+ msg.tags.clear()
+ msg.tags.add('foo')
+
+ Using This would ensure the message never ends up with no tags
+ applied at all.
+
+ It is safe to nest calls to this context manager.
+
+ :raises ReadOnlyDatabaseError: if the database is opened in
+ read-only mode.
+ :raises UnbalancedFreezeThawError: if you somehow managed to
+ call __exit__ of this context manager more than once. Why
+ did you do that?
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_message_freeze(self._msg_p)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ self._frozen = True
+ try:
+ yield
+ except Exception:
+ # Only way to "rollback" these changes is to destroy
+ # ourselves and re-create. Behold.
+ msgid = self.messageid
+ self._destroy()
+ with contextlib.suppress(Exception):
+ new = self._db.find(msgid)
+ self._msg_p = new._msg_p
+ new._msg_p = None
+ del new
+ raise
+ else:
+ ret = capi.lib.notmuch_message_thaw(self._msg_p)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ self._frozen = False
+
+ @property
+ def properties(self):
+ """A map of arbitrary key-value pairs associated with the message.
+
+ Be aware that properties may be used by other extensions to
+ store state in. So delete or modify with care.
+
+ The properties map is somewhat special. It is essentially a
+ multimap-like structure where each key can have multiple
+ values. Therefore accessing a single item using
+ :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
+ will only return you the *first* item if there are multiple
+ and thus are only recommended if you know there to be only one
+ value.
+
+ Instead the map has an additional :meth:`PropertiesMap.all`
+ method which can be used to retrieve all properties of a given
+ key. This method also allows iterating of a a subset of the
+ keys starting with a given prefix.
+ """
+ try:
+ ref = self._cached_props
+ except AttributeError:
+ props = None
+ else:
+ props = ref()
+ if props is None:
+ props = PropertiesMap(self, '_msg_p')
+ self._cached_props = weakref.ref(props)
+ return props
+
+ def replies(self):
+ """Return an iterator of all replies to this message.
+
+ This method will only work if the message was created from a
+ thread. Otherwise it will yield no results.
+
+ :returns: An iterator yielding :class:`Message` instances.
+ :rtype: MessageIter
+ """
+ # The notmuch_messages_valid call accepts NULL and this will
+ # become an empty iterator, raising StopIteration immediately.
+ # Hence no return value checking here.
+ msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
+ return MessageIter(self, msgs_p, db=self._db)
+
+ def __hash__(self):
+ return hash(self.messageid)
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.messageid == other.messageid
+
+
+class FilenamesIter(base.NotmuchIter):
+ """Iterator for binary filenames objects."""
+
+ def __init__(self, parent, iter_p):
+ super().__init__(parent, iter_p,
+ fn_destroy=capi.lib.notmuch_filenames_destroy,
+ fn_valid=capi.lib.notmuch_filenames_valid,
+ fn_get=capi.lib.notmuch_filenames_get,
+ fn_next=capi.lib.notmuch_filenames_move_to_next)
+
+ def __next__(self):
+ fname = super().__next__()
+ return capi.ffi.string(fname)
+
+
+class PathIter(FilenamesIter):
+ """Iterator for pathlib.Path objects."""
+
+ def __next__(self):
+ fname = super().__next__()
+ return pathlib.Path(os.fsdecode(fname))
+
+
+class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
+ """A mutable mapping to manage properties.
+
+ Both keys and values of properties are supposed to be UTF-8
+ strings in libnotmuch. However since the uderlying API uses
+ bytestrings you can use either str or bytes to represent keys and
+ all returned keys and values use :class:`BinString`.
+
+ Also be aware that ``iter(this_map)`` will return duplicate keys,
+ while the :class:`collections.abc.KeysView` returned by
+ :meth:`keys` is a :class:`collections.abc.Set` subclass. This
+ means the former will yield duplicate keys while the latter won't.
+ It also means ``len(list(iter(this_map)))`` could be different
+ than ``len(this_map.keys())``. ``len(this_map)`` will correspond
+ with the lenght of the default iterator.
+
+ Be aware that libnotmuch exposes all of this as iterators, so
+ quite a few operations have O(n) performance instead of the usual
+ O(1).
+ """
+ Property = collections.namedtuple('Property', ['key', 'value'])
+ _marker = object()
+
+ def __init__(self, msg, ptr_name):
+ self._msg = msg
+ self._ptr = lambda: getattr(msg, ptr_name)
+
+ @property
+ def alive(self):
+ if not self._msg.alive:
+ return False
+ try:
+ self._ptr
+ except errors.ObjectDestroyedError:
+ return False
+ else:
+ return True
+
+ def _destroy(self):
+ pass
+
+ def __iter__(self):
+ """Return an iterator which iterates over the keys.
+
+ Be aware that a single key may have multiple values associated
+ with it, if so it will appear multiple times here.
+ """
+ iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+ return PropertiesKeyIter(self, iter_p)
+
+ def __len__(self):
+ iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+ it = base.NotmuchIter(
+ self, iter_p,
+ fn_destroy=capi.lib.notmuch_message_properties_destroy,
+ fn_valid=capi.lib.notmuch_message_properties_valid,
+ fn_get=capi.lib.notmuch_message_properties_key,
+ fn_next=capi.lib.notmuch_message_properties_move_to_next,
+ )
+ return len(list(it))
+
+ def __getitem__(self, key):
+ """Return **the first** peroperty associated with a key."""
+ if isinstance(key, str):
+ key = key.encode('utf-8')
+ value_pp = capi.ffi.new('char**')
+ ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ if value_pp[0] == capi.ffi.NULL:
+ raise KeyError
+ return base.BinString.from_cffi(value_pp[0])
+
+ def keys(self):
+ """Return a :class:`collections.abc.KeysView` for this map.
+
+ Even when keys occur multiple times this is a subset of set()
+ so will only contain them once.
+ """
+ return collections.abc.KeysView({k: None for k in self})
+
+ def items(self):
+ """Return a :class:`collections.abc.ItemsView` for this map.
+
+ The ItemsView treats a ``(key, value)`` pair as unique, so
+ dupcliate ``(key, value)`` pairs will be merged together.
+ However duplicate keys with different values will be returned.
+ """
+ items = set()
+ props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+ while capi.lib.notmuch_message_properties_valid(props_p):
+ key = capi.lib.notmuch_message_properties_key(props_p)
+ value = capi.lib.notmuch_message_properties_value(props_p)
+ items.add((base.BinString.from_cffi(key),
+ base.BinString.from_cffi(value)))
+ capi.lib.notmuch_message_properties_move_to_next(props_p)
+ capi.lib.notmuch_message_properties_destroy(props_p)
+ return PropertiesItemsView(items)
+
+ def values(self):
+ """Return a :class:`collecions.abc.ValuesView` for this map.
+
+ All unique property values are included in the view.
+ """
+ values = set()
+ props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+ while capi.lib.notmuch_message_properties_valid(props_p):
+ value = capi.lib.notmuch_message_properties_value(props_p)
+ values.add(base.BinString.from_cffi(value))
+ capi.lib.notmuch_message_properties_move_to_next(props_p)
+ capi.lib.notmuch_message_properties_destroy(props_p)
+ return PropertiesValuesView(values)
+
+ def __setitem__(self, key, value):
+ """Add a key-value pair to the properties.
+
+ You may prefer to use :meth:`add` for clarity since this
+ method usually implies implicit overwriting of an existing key
+ if it exists, while for properties this is not the case.
+ """
+ self.add(key, value)
+
+ def add(self, key, value):
+ """Add a key-value pair to the properties."""
+ if isinstance(key, str):
+ key = key.encode('utf-8')
+ if isinstance(value, str):
+ value = value.encode('utf-8')
+ ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def __delitem__(self, key):
+ """Remove all properties with this key."""
+ if isinstance(key, str):
+ key = key.encode('utf-8')
+ ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def remove(self, key, value):
+ """Remove a key-value pair from the properties."""
+ if isinstance(key, str):
+ key = key.encode('utf-8')
+ if isinstance(value, str):
+ value = value.encode('utf-8')
+ ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def pop(self, key, default=_marker):
+ try:
+ value = self[key]
+ except KeyError:
+ if default is self._marker:
+ raise
+ else:
+ return default
+ else:
+ self.remove(key, value)
+ return value
+
+ def popitem(self):
+ try:
+ key = next(iter(self))
+ except StopIteration:
+ raise KeyError
+ value = self.pop(key)
+ return (key, value)
+
+ def clear(self):
+ ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
+ capi.ffi.NULL)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def getall(self, prefix='', *, exact=False):
+ """Return an iterator yielding all properties for a given key prefix.
+
+ The returned iterator yields all peroperties which start with
+ a given key prefix as ``(key, value)`` namedtuples. If called
+ with ``exact=True`` then only properties which exactly match
+ the prefix are returned, those a key longer than the prefix
+ will not be included.
+
+ :param prefix: The prefix of the key.
+ """
+ if isinstance(prefix, str):
+ prefix = prefix.encode('utf-8')
+ props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
+ prefix, exact)
+ return PropertiesIter(self, props_p)
+
+
+class PropertiesKeyIter(base.NotmuchIter):
+
+ def __init__(self, parent, iter_p):
+ super().__init__(
+ parent,
+ iter_p,
+ fn_destroy=capi.lib.notmuch_message_properties_destroy,
+ fn_valid=capi.lib.notmuch_message_properties_valid,
+ fn_get=capi.lib.notmuch_message_properties_key,
+ fn_next=capi.lib.notmuch_message_properties_move_to_next)
+
+ def __next__(self):
+ item = super().__next__()
+ return base.BinString.from_cffi(item)
+
+
+class PropertiesIter(base.NotmuchIter):
+
+ def __init__(self, parent, iter_p):
+ super().__init__(
+ parent,
+ iter_p,
+ fn_destroy=capi.lib.notmuch_message_properties_destroy,
+ fn_valid=capi.lib.notmuch_message_properties_valid,
+ fn_get=capi.lib.notmuch_message_properties_key,
+ fn_next=capi.lib.notmuch_message_properties_move_to_next,
+ )
+
+ def __next__(self):
+ if not self._fn_valid(self._iter_p):
+ self._destroy()
+ raise StopIteration
+ key = capi.lib.notmuch_message_properties_key(self._iter_p)
+ value = capi.lib.notmuch_message_properties_value(self._iter_p)
+ capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
+ return PropertiesMap.Property(base.BinString.from_cffi(key),
+ base.BinString.from_cffi(value))
+
+
+class PropertiesItemsView(collections.abc.Set):
+
+ __slots__ = ('_items',)
+
+ def __init__(self, items):
+ self._items = items
+
+ @classmethod
+ def _from_iterable(self, it):
+ return set(it)
+
+ def __len__(self):
+ return len(self._items)
+
+ def __contains__(self, item):
+ return item in self._items
+
+ def __iter__(self):
+ yield from self._items
+
+
+collections.abc.ItemsView.register(PropertiesItemsView)
+
+
+class PropertiesValuesView(collections.abc.Set):
+
+ __slots__ = ('_values',)
+
+ def __init__(self, values):
+ self._values = values
+
+ def __len__(self):
+ return len(self._values)
+
+ def __contains__(self, value):
+ return value in self._values
+
+ def __iter__(self):
+ yield from self._values
+
+
+collections.abc.ValuesView.register(PropertiesValuesView)
+
+
+class MessageIter(base.NotmuchIter):
+
+ def __init__(self, parent, msgs_p, *, db):
+ self._db = db
+ super().__init__(parent, msgs_p,
+ fn_destroy=capi.lib.notmuch_messages_destroy,
+ fn_valid=capi.lib.notmuch_messages_valid,
+ fn_get=capi.lib.notmuch_messages_get,
+ fn_next=capi.lib.notmuch_messages_move_to_next)
+
+ def __next__(self):
+ msg_p = super().__next__()
+ return Message(self, msg_p, db=self._db)
--- /dev/null
+from notmuch2 import _base as base
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+from notmuch2 import _message as message
+from notmuch2 import _thread as thread
+
+
+__all__ = []
+
+
+class Query(base.NotmuchObject):
+ """Private, minimal query object.
+
+ This is not meant for users and is not a full implementation of
+ the query API. It is only an intermediate used internally to
+ match libnotmuch's memory management.
+ """
+ _query_p = base.MemoryPointer()
+
+ def __init__(self, db, query_p):
+ self._db = db
+ self._query_p = query_p
+
+ @property
+ def alive(self):
+ if not self._db.alive:
+ return False
+ try:
+ self._query_p
+ except errors.ObjectDestroyedError:
+ return False
+ else:
+ return True
+
+ def __del__(self):
+ self._destroy()
+
+ def _destroy(self):
+ if self.alive:
+ capi.lib.notmuch_query_destroy(self._query_p)
+ self._query_p = None
+
+ @property
+ def query(self):
+ """The query string as seen by libnotmuch."""
+ q = capi.lib.notmuch_query_get_query_string(self._query_p)
+ return base.BinString.from_cffi(q)
+
+ def messages(self):
+ """Return an iterator over all the messages found by the query.
+
+ This executes the query and returns an iterator over the
+ :class:`Message` objects found.
+ """
+ msgs_pp = capi.ffi.new('notmuch_messages_t**')
+ ret = capi.lib.notmuch_query_search_messages(self._query_p, msgs_pp)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ return message.MessageIter(self, msgs_pp[0], db=self._db)
+
+ def count_messages(self):
+ """Return the number of messages matching this query."""
+ count_p = capi.ffi.new('unsigned int *')
+ ret = capi.lib.notmuch_query_count_messages(self._query_p, count_p)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ return count_p[0]
+
+ def threads(self):
+ """Return an iterator over all the threads found by the query."""
+ threads_pp = capi.ffi.new('notmuch_threads_t **')
+ ret = capi.lib.notmuch_query_search_threads(self._query_p, threads_pp)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ return thread.ThreadIter(self, threads_pp[0], db=self._db)
+
+ def count_threads(self):
+ """Return the number of threads matching this query."""
+ count_p = capi.ffi.new('unsigned int *')
+ ret = capi.lib.notmuch_query_count_threads(self._query_p, count_p)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+ return count_p[0]
--- /dev/null
+import collections.abc
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+
+
+__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
+
+
+class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
+ """The tags associated with a message thread or whole database.
+
+ Both a thread as well as the database expose the union of all tags
+ in messages associated with them. This exposes these as a
+ :class:`collections.abc.Set` object.
+
+ Note that due to the underlying notmuch API the performance of the
+ implementation is not the same as you would expect from normal
+ sets. E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
+ rather then O(1).
+
+ Tags are internally stored as bytestrings but normally exposed as
+ unicode strings using the UTF-8 encoding and the *ignore* decoder
+ error handler. However the :meth:`iter` method can be used to
+ return tags as bytestrings or using a different error handler.
+
+ Note that when doing arithmetic operations on tags, this class
+ will return a plain normal set as it is no longer associated with
+ the message.
+
+ :param parent: the parent object
+ :param ptr_name: the name of the attribute on the parent which will
+ return the memory pointer. This allows this object to
+ access the pointer via the parent's descriptor and thus
+ trigger :class:`MemoryPointer`'s memory safety.
+ :param cffi_fn: the callable CFFI wrapper to retrieve the tags
+ iter. This can be one of notmuch_database_get_all_tags,
+ notmuch_thread_get_tags or notmuch_message_get_tags.
+ """
+
+ def __init__(self, parent, ptr_name, cffi_fn):
+ self._parent = parent
+ self._ptr = lambda: getattr(parent, ptr_name)
+ self._cffi_fn = cffi_fn
+
+ def __del__(self):
+ self._destroy()
+
+ @property
+ def alive(self):
+ return self._parent.alive
+
+ def _destroy(self):
+ pass
+
+ @classmethod
+ def _from_iterable(cls, it):
+ return set(it)
+
+ def __iter__(self):
+ """Return an iterator over the tags.
+
+ Tags are yielded as unicode strings, decoded using the
+ "ignore" error handler.
+
+ :raises NullPointerError: If the iterator can not be created.
+ """
+ return self.iter(encoding='utf-8', errors='ignore')
+
+ def iter(self, *, encoding=None, errors='strict'):
+ """Aternate iterator constructor controlling string decoding.
+
+ Tags are stored as bytes in the notmuch database, in Python
+ it's easier to work with unicode strings and thus is what the
+ normal iterator returns. However this method allows you to
+ specify how you would like to get the tags, defaulting to the
+ bytestring representation instead of unicode strings.
+
+ :param encoding: Which codec to use. The default *None* does not
+ decode at all and will return the unmodified bytes.
+ Otherwise this is passed on to :func:`str.decode`.
+ :param errors: If using a codec, this is the error handler.
+ See :func:`str.decode` to which this is passed on.
+
+ :raises NullPointerError: When things do not go as planned.
+ """
+ # self._cffi_fn should point either to
+ # notmuch_database_get_all_tags, notmuch_thread_get_tags or
+ # notmuch_message_get_tags. nothmuch.h suggests these never
+ # fail, let's handle NULL anyway.
+ tags_p = self._cffi_fn(self._ptr())
+ if tags_p == capi.ffi.NULL:
+ raise errors.NullPointerError()
+ tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
+ return tags
+
+ def __len__(self):
+ return sum(1 for t in self)
+
+ def __contains__(self, tag):
+ if isinstance(tag, str):
+ tag = tag.encode()
+ for msg_tag in self.iter():
+ if tag == msg_tag:
+ return True
+ else:
+ return False
+
+ def __eq__(self, other):
+ return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
+
+ def __hash__(self):
+ return hash(tuple(self.iter()))
+
+ def __repr__(self):
+ return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
+ name=self.__class__.__name__,
+ addr=id(self),
+ tags=', '.join(repr(t) for t in self))
+
+
+class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
+ """The tags associated with a message.
+
+ This is a :class:`collections.abc.MutableSet` object which can be
+ used to manipulate the tags of a message.
+
+ Note that due to the underlying notmuch API the performance of the
+ implementation is not the same as you would expect from normal
+ sets. E.g. the ``in`` operator and variants are O(n) rather then
+ O(1).
+
+ Tags are bytestrings and calling ``iter()`` will return an
+ iterator yielding bytestrings. However the :meth:`iter` method
+ can be used to return tags as unicode strings, while all other
+ operations accept either byestrings or unicode strings. In case
+ unicode strings are used they will be encoded using utf-8 before
+ being passed to notmuch.
+ """
+
+ # Since we subclass ImmutableTagSet we inherit a __hash__. But we
+ # are mutable, setting it to None will make the Python machinary
+ # recognise us as unhashable.
+ __hash__ = None
+
+ def add(self, tag):
+ """Add a tag to the message.
+
+ :param tag: The tag to add.
+ :type tag: str or bytes. A str will be encoded using UTF-8.
+
+ :param sync_flags: Whether to sync the maildir flags with the
+ new set of tags. Leaving this as *None* respects the
+ configuration set in the database, while *True* will always
+ sync and *False* will never sync.
+ :param sync_flags: NoneType or bool
+
+ :raises TypeError: If the tag is not a valid type.
+ :raises TagTooLongError: If the added tag exceeds the maximum
+ lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
+ :raises ReadOnlyDatabaseError: If the database is opened in
+ read-only mode.
+ """
+ if isinstance(tag, str):
+ tag = tag.encode()
+ if not isinstance(tag, bytes):
+ raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
+ ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def discard(self, tag):
+ """Remove a tag from the message.
+
+ :param tag: The tag to remove.
+ :type tag: str of bytes. A str will be encoded using UTF-8.
+ :param sync_flags: Whether to sync the maildir flags with the
+ new set of tags. Leaving this as *None* respects the
+ configuration set in the database, while *True* will always
+ sync and *False* will never sync.
+ :param sync_flags: NoneType or bool
+
+ :raises TypeError: If the tag is not a valid type.
+ :raises TagTooLongError: If the tag exceeds the maximum
+ lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
+ :raises ReadOnlyDatabaseError: If the database is opened in
+ read-only mode.
+ """
+ if isinstance(tag, str):
+ tag = tag.encode()
+ if not isinstance(tag, bytes):
+ raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
+ ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def clear(self):
+ """Remove all tags from the message.
+
+ :raises ReadOnlyDatabaseError: If the database is opened in
+ read-only mode.
+ """
+ ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def from_maildir_flags(self):
+ """Update the tags based on the state in the message's maildir flags.
+
+ This function examines the filenames of 'message' for maildir
+ flags, and adds or removes tags on 'message' as follows when
+ these flags are present:
+
+ Flag Action if present
+ ---- -----------------
+ 'D' Adds the "draft" tag to the message
+ 'F' Adds the "flagged" tag to the message
+ 'P' Adds the "passed" tag to the message
+ 'R' Adds the "replied" tag to the message
+ 'S' Removes the "unread" tag from the message
+
+ For each flag that is not present, the opposite action
+ (add/remove) is performed for the corresponding tags.
+
+ Flags are identified as trailing components of the filename
+ after a sequence of ":2,".
+
+ If there are multiple filenames associated with this message,
+ the flag is considered present if it appears in one or more
+ filenames. (That is, the flags from the multiple filenames are
+ combined with the logical OR operator.)
+ """
+ ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+ def to_maildir_flags(self):
+ """Update the message's maildir flags based on the notmuch tags.
+
+ If the message's filename is in a maildir directory, that is a
+ directory named ``new`` or ``cur``, and has a valid maildir
+ filename then the flags will be added as such:
+
+ 'D' if the message has the "draft" tag
+ 'F' if the message has the "flagged" tag
+ 'P' if the message has the "passed" tag
+ 'R' if the message has the "replied" tag
+ 'S' if the message does not have the "unread" tag
+
+ Any existing flags unmentioned in the list above will be
+ preserved in the renaming.
+
+ Also, if this filename is in a directory named "new", rename it to
+ be within the neighboring directory named "cur".
+
+ In case there are multiple files associated with the message
+ all filenames will get the same logic applied.
+ """
+ ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
+ if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(ret)
+
+
+class TagsIter(base.NotmuchObject, collections.abc.Iterator):
+ """Iterator over tags.
+
+ This is only an interator, not a container so calling
+ :meth:`__iter__` does not return a new, replenished iterator but
+ only itself.
+
+ :param parent: The parent object to keep alive.
+ :param tags_p: The CFFI pointer to the C-level tags iterator.
+ :param encoding: Which codec to use. The default *None* does not
+ decode at all and will return the unmodified bytes.
+ Otherwise this is passed on to :func:`str.decode`.
+ :param errors: If using a codec, this is the error handler.
+ See :func:`str.decode` to which this is passed on.
+
+ :raises ObjectDestoryedError: if used after destroyed.
+ """
+ _tags_p = base.MemoryPointer()
+
+ def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
+ self._parent = parent
+ self._tags_p = tags_p
+ self._encoding = encoding
+ self._errors = errors
+
+ def __del__(self):
+ self._destroy()
+
+ @property
+ def alive(self):
+ if not self._parent.alive:
+ return False
+ try:
+ self._tags_p
+ except errors.ObjectDestroyedError:
+ return False
+ else:
+ return True
+
+ def _destroy(self):
+ if self.alive:
+ try:
+ capi.lib.notmuch_tags_destroy(self._tags_p)
+ except errors.ObjectDestroyedError:
+ pass
+ self._tags_p = None
+
+ def __iter__(self):
+ """Return the iterator itself.
+
+ Note that as this is an iterator and not a container this will
+ not return a new iterator. Thus any elements already consumed
+ will not be yielded by the :meth:`__next__` method anymore.
+ """
+ return self
+
+ def __next__(self):
+ if not capi.lib.notmuch_tags_valid(self._tags_p):
+ self._destroy()
+ raise StopIteration()
+ tag_p = capi.lib.notmuch_tags_get(self._tags_p)
+ tag = capi.ffi.string(tag_p)
+ if self._encoding:
+ tag = tag.decode(encoding=self._encoding, errors=self._errors)
+ capi.lib.notmuch_tags_move_to_next(self._tags_p)
+ return tag
+
+ def __repr__(self):
+ try:
+ self._tags_p
+ except errors.ObjectDestroyedError:
+ return '<TagsIter (exhausted)>'
+ else:
+ return '<TagsIter>'
--- /dev/null
+import collections.abc
+import weakref
+
+from notmuch2 import _base as base
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+from notmuch2 import _message as message
+from notmuch2 import _tags as tags
+
+
+__all__ = ['Thread']
+
+
+class Thread(base.NotmuchObject, collections.abc.Iterable):
+ _thread_p = base.MemoryPointer()
+
+ def __init__(self, parent, thread_p, *, db):
+ self._parent = parent
+ self._thread_p = thread_p
+ self._db = db
+
+ @property
+ def alive(self):
+ if not self._parent.alive:
+ return False
+ try:
+ self._thread_p
+ except errors.ObjectDestroyedError:
+ return False
+ else:
+ return True
+
+ def __del__(self):
+ self._destroy()
+
+ def _destroy(self):
+ if self.alive:
+ capi.lib.notmuch_thread_destroy(self._thread_p)
+ self._thread_p = None
+
+ @property
+ def threadid(self):
+ """The thread ID as a :class:`BinString`.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_thread_get_thread_id(self._thread_p)
+ return base.BinString.from_cffi(ret)
+
+ def __len__(self):
+ """Return the number of messages in the thread.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ return capi.lib.notmuch_thread_get_total_messages(self._thread_p)
+
+ def toplevel(self):
+ """Return an iterator of the toplevel messages.
+
+ :returns: An iterator yielding :class:`Message` instances.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ msgs_p = capi.lib.notmuch_thread_get_toplevel_messages(self._thread_p)
+ return message.MessageIter(self, msgs_p, db=self._db)
+
+ def __iter__(self):
+ """Return an iterator over all the messages in the thread.
+
+ :returns: An iterator yielding :class:`Message` instances.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ msgs_p = capi.lib.notmuch_thread_get_messages(self._thread_p)
+ return message.MessageIter(self, msgs_p, db=self._db)
+
+ @property
+ def matched(self):
+ """The number of messages in this thread which matched the query.
+
+ Of the messages in the thread this gives the count of messages
+ which did directly match the search query which this thread
+ originates from.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ return capi.lib.notmuch_thread_get_matched_messages(self._thread_p)
+
+ @property
+ def authors(self):
+ """A comma-separated string of all authors in the thread.
+
+ Authors of messages which matched the query the thread was
+ retrieved from will be at the head of the string, ordered by
+ date of their messages. Following this will be the authors of
+ the other messages in the thread, also ordered by date of
+ their messages. Both groups of authors are separated by the
+ ``|`` character.
+
+ :returns: The stringified list of authors.
+ :rtype: BinString
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_thread_get_authors(self._thread_p)
+ return base.BinString.from_cffi(ret)
+
+ @property
+ def subject(self):
+ """The subject of the thread, taken from the first message.
+
+ The thread's subject is taken to be the subject of the first
+ message according to query sort order.
+
+ :returns: The thread's subject.
+ :rtype: BinString
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ ret = capi.lib.notmuch_thread_get_subject(self._thread_p)
+ return base.BinString.from_cffi(ret)
+
+ @property
+ def first(self):
+ """Return the date of the oldest message in the thread.
+
+ The time the first message was sent as an integer number of
+ seconds since the *epoch*, 1 Jan 1970.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ return capi.lib.notmuch_thread_get_oldest_date(self._thread_p)
+
+ @property
+ def last(self):
+ """Return the date of the newest message in the thread.
+
+ The time the last message was sent as an integer number of
+ seconds since the *epoch*, 1 Jan 1970.
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ return capi.lib.notmuch_thread_get_newest_date(self._thread_p)
+
+ @property
+ def tags(self):
+ """Return an immutable set with all tags used in this thread.
+
+ This returns an immutable set-like object implementing the
+ collections.abc.Set Abstract Base Class. Due to the
+ underlying libnotmuch implementation some operations have
+ different performance characteristics then plain set objects.
+ Mainly any lookup operation is O(n) rather then O(1).
+
+ Normal usage treats tags as UTF-8 encoded unicode strings so
+ they are exposed to Python as normal unicode string objects.
+ If you need to handle tags stored in libnotmuch which are not
+ valid unicode do check the :class:`ImmutableTagSet` docs for
+ how to handle this.
+
+ :rtype: ImmutableTagSet
+
+ :raises ObjectDestroyedError: if used after destoryed.
+ """
+ try:
+ ref = self._cached_tagset
+ except AttributeError:
+ tagset = None
+ else:
+ tagset = ref()
+ if tagset is None:
+ tagset = tags.ImmutableTagSet(
+ self, '_thread_p', capi.lib.notmuch_thread_get_tags)
+ self._cached_tagset = weakref.ref(tagset)
+ return tagset
+
+
+class ThreadIter(base.NotmuchIter):
+
+ def __init__(self, parent, threads_p, *, db):
+ self._db = db
+ super().__init__(parent, threads_p,
+ fn_destroy=capi.lib.notmuch_threads_destroy,
+ fn_valid=capi.lib.notmuch_threads_valid,
+ fn_get=capi.lib.notmuch_threads_get,
+ fn_next=capi.lib.notmuch_threads_move_to_next)
+
+ def __next__(self):
+ thread_p = super().__next__()
+ return Thread(self, thread_p, db=self._db)
setuptools.setup(
- name='notdb',
+ name='notmuch2',
version='0.1',
description='Pythonic bindings for the notmuch mail database using CFFI',
author='Floris Bruynooghe',
setup_requires=['cffi>=1.0.0'],
install_requires=['cffi>=1.0.0'],
packages=setuptools.find_packages(exclude=['tests']),
- cffi_modules=['notdb/_build.py:ffibuilder'],
+ cffi_modules=['notmuch2/_build.py:ffibuilder'],
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
import pytest
-from notdb import _base as base
-from notdb import _errors as errors
+from notmuch2 import _base as base
+from notmuch2 import _errors as errors
class TestNotmuchObject:
import pytest
-import notdb
-import notdb._errors as errors
-import notdb._database as dbmod
-import notdb._message as message
+import notmuch2
+import notmuch2._errors as errors
+import notmuch2._database as dbmod
+import notmuch2._message as message
@pytest.fixture
@pytest.fixture
def db(self, maildir, notmuch):
- """Return a read-only notdb.Database.
+ """Return a read-only notmuch2.Database.
The database will have 3 messages, 2 threads.
"""
def test_message_match(self, db):
msgs = db.messages('*')
msg = next(msgs)
- assert isinstance(msg, notdb.Message)
+ assert isinstance(msg, notmuch2.Message)
def test_count_threads(self, db):
assert db.count_threads('*') == 2
def test_threads_match(self, db):
threads = db.threads('*')
thread = next(threads)
- assert isinstance(thread, notdb.Thread)
+ assert isinstance(thread, notmuch2.Thread)
import pytest
-import notdb
+import notmuch2
class TestMessage:
@pytest.fixture
def db(self, maildir):
- with notdb.Database.create(maildir.path) as db:
+ with notmuch2.Database.create(maildir.path) as db:
yield db
@pytest.fixture
yield msg
def test_type(self, msg):
- assert isinstance(msg, notdb.NotmuchObject)
- assert isinstance(msg, notdb.Message)
+ assert isinstance(msg, notmuch2.NotmuchObject)
+ assert isinstance(msg, notmuch2.Message)
def test_alive(self, msg):
assert msg.alive
def test_messageid_type(self, msg):
assert isinstance(msg.messageid, str)
- assert isinstance(msg.messageid, notdb.BinString)
+ assert isinstance(msg.messageid, notmuch2.BinString)
assert isinstance(bytes(msg.messageid), bytes)
def test_messageid(self, msg, maildir_msg):
def test_threadid_type(self, msg):
assert isinstance(msg.threadid, str)
- assert isinstance(msg.threadid, notdb.BinString)
+ assert isinstance(msg.threadid, notmuch2.BinString)
assert isinstance(bytes(msg.threadid), bytes)
def test_path_type(self, msg):
@pytest.fixture
def props(self, maildir):
msgid, path = maildir.deliver()
- with notdb.Database.create(maildir.path) as db:
+ with notmuch2.Database.create(maildir.path) as db:
msg, dup = db.add(path, sync_flags=False)
yield msg.properties
import pytest
-from notdb import _database as database
-from notdb import _tags as tags
+from notmuch2 import _database as database
+from notmuch2 import _tags as tags
class TestImmutable:
import pytest
-import notdb
+import notmuch2
@pytest.fixture
maildir.deliver(body='bar',
headers=[('In-Reply-To', '<{}>'.format(msgid))])
notmuch('new')
- with notdb.Database(maildir.path) as db:
+ with notmuch2.Database(maildir.path) as db:
yield next(db.threads('foo'))
def test_type(thread):
- assert isinstance(thread, notdb.Thread)
+ assert isinstance(thread, notmuch2.Thread)
assert isinstance(thread, collections.abc.Iterable)
def test_threadid(thread):
- assert isinstance(thread.threadid, notdb.BinString)
+ assert isinstance(thread.threadid, notmuch2.BinString)
assert thread.threadid
def test_toplevel(thread):
msgs = thread.toplevel()
- assert isinstance(next(msgs), notdb.Message)
+ assert isinstance(next(msgs), notmuch2.Message)
with pytest.raises(StopIteration):
next(msgs)
def test_toplevel_reply(thread):
msg = next(thread.toplevel())
- assert isinstance(next(msg.replies()), notdb.Message)
+ assert isinstance(next(msg.replies()), notmuch2.Message)
def test_iter(thread):
msgs = list(iter(thread))
assert len(msgs) == len(thread)
for msg in msgs:
- assert isinstance(msg, notdb.Message)
+ assert isinstance(msg, notmuch2.Message)
def test_matched(thread):
def test_authors_type(thread):
- assert isinstance(thread.authors, notdb.BinString)
+ assert isinstance(thread.authors, notmuch2.BinString)
def test_authors(thread):
def test_tags_type(thread):
- assert isinstance(thread.tags, notdb.ImmutableTagSet)
+ assert isinstance(thread.tags, notmuch2.ImmutableTagSet)
def test_tags_cache(thread):
[pytest]
minversion = 3.0
-addopts = -ra --cov=notdb --cov=tests
+addopts = -ra --cov=notmuch2 --cov=tests
[tox]
envlist = py35,py36,py37,pypy35,pypy36
cffi
pytest
pytest-cov
-commands = pytest --cov={envsitepackagesdir}/notdb {posargs}
+commands = pytest --cov={envsitepackagesdir}/notmuch2 {posargs}
[testenv:pypy35]
basepython = pypy3.5