+++ /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)