7 import notmuch2._base as base
8 import notmuch2._capi as capi
9 import notmuch2._errors as errors
10 import notmuch2._tags as tags
16 class Message(base.NotmuchObject):
17 """An email message stored in the notmuch database retrieved via a query.
19 This should not be directly created, instead it will be returned
20 by calling methods on :class:`Database`. A message keeps a
21 reference to the database object since the database object can not
22 be released while the message is in use.
24 Note that this represents a message in the notmuch database. For
25 full email functionality you may want to use the :mod:`email`
26 package from Python's standard library. You could e.g. create
29 notmuch_msg = db.get_message(msgid) # or from a query
30 parser = email.parser.BytesParser(policy=email.policy.default)
31 with notmuch_msg.path.open('rb) as fp:
32 email_msg = parser.parse(fp)
34 Most commonly the functionality provided by notmuch is sufficient
35 to read email however.
37 Messages are considered equal when they have the same message ID.
38 This is how libnotmuch treats messages as well, the
39 :meth:`pathnames` function returns multiple results for
42 :param parent: The parent object. This is probably one off a
43 :class:`Database`, :class:`Thread` or :class:`Query`.
44 :type parent: NotmuchObject
45 :param db: The database instance this message is associated with.
46 This could be the same as the parent.
48 :param msg_p: The C pointer to the ``notmuch_message_t``.
50 :param dup: Whether the message was a duplicate on insertion.
51 :type dup: None or bool
53 _msg_p = base.MemoryPointer()
55 def __init__(self, parent, msg_p, *, db):
62 if not self._parent.alive:
66 except errors.ObjectDestroyedError:
76 capi.lib.notmuch_message_destroy(self._msg_p)
81 """The message ID as a string.
83 The message ID is decoded with the ignore error handler. This
84 is fine as long as the message ID is well formed. If it is
85 not valid ASCII then this will be lossy. So if you need to be
86 able to write the exact same message ID back you should use
89 Note that notmuch will decode the message ID value and thus
90 strip off the surrounding ``<`` and ``>`` characters. This is
91 different from Python's :mod:`email` package behaviour which
92 leaves these characters in place.
94 :returns: The message ID.
95 :rtype: :class:`BinString`, this is a normal str but calling
96 bytes() on it will return the original bytes used to create
99 :raises ObjectDestroyedError: if used after destroyed.
101 ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
102 return base.BinString(capi.ffi.string(ret))
108 The thread ID is decoded with the surrogateescape error
109 handler so that it is possible to reconstruct the original
110 thread ID if it is not valid UTF-8.
112 :returns: The thread ID.
113 :rtype: :class:`BinString`, this is a normal str but calling
114 bytes() on it will return the original bytes used to create
117 :raises ObjectDestroyedError: if used after destroyed.
119 ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
120 return base.BinString(capi.ffi.string(ret))
124 """A pathname of the message as a pathlib.Path instance.
126 If multiple files in the database contain the same message ID
127 this will be just one of the files, chosen at random.
129 :raises ObjectDestroyedError: if used after destroyed.
131 ret = capi.lib.notmuch_message_get_filename(self._msg_p)
132 return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
136 """A pathname of the message as a bytes object.
138 See :attr:`path` for details, this is the same but does return
139 the path as a bytes object which is faster but less convenient.
141 :raises ObjectDestroyedError: if used after destroyed.
143 ret = capi.lib.notmuch_message_get_filename(self._msg_p)
144 return capi.ffi.string(ret)
147 """Return an iterator of all files for this message.
149 If multiple files contained the same message ID they will all
150 be returned here. The files are returned as instances of
151 :class:`pathlib.Path`.
153 :returns: Iterator yielding :class:`pathlib.Path` instances.
156 :raises ObjectDestroyedError: if used after destroyed.
158 fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
159 return PathIter(self, fnames_p)
161 def filenamesb(self):
162 """Return an iterator of all files for this message.
164 This is like :meth:`pathnames` but the files are returned as
165 byte objects instead.
167 :returns: Iterator yielding :class:`bytes` instances.
170 :raises ObjectDestroyedError: if used after destroyed.
172 fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
173 return FilenamesIter(self, fnames_p)
177 """Indicates whether this message is a ghost message.
179 A ghost message if a message which we know exists, but it has
180 no files or content associated with it. This can happen if
181 it was referenced by some other message. Only the
182 :attr:`messageid` and :attr:`threadid` attributes are valid
185 :raises ObjectDestroyedError: if used after destroyed.
187 ret = capi.lib.notmuch_message_get_flag(
188 self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
193 """Indicates whether this message was excluded from the query.
195 When a message is created from a search, sometimes messages
196 that where excluded by the search query could still be
197 returned by it, e.g. because they are part of a thread
198 matching the query. the :meth:`Database.query` method allows
199 these messages to be flagged, which results in this property
202 :raises ObjectDestroyedError: if used after destroyed.
204 ret = capi.lib.notmuch_message_get_flag(
205 self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
210 """The message date as an integer.
212 The time the message was sent as an integer number of seconds
213 since the *epoch*, 1 Jan 1970. This is derived from the
214 message's header, you can get the original header value with
217 :raises ObjectDestroyedError: if used after destroyed.
219 return capi.lib.notmuch_message_get_date(self._msg_p)
221 def header(self, name):
222 """Return the value of the named header.
224 Returns the header from notmuch, some common headers are
225 stored in the database, others are read from the file.
226 Headers are returned with their newlines stripped and
227 collapsed concatenated together if they occur multiple times.
228 You may be better off using the standard library email
229 package's ``email.message_from_file(msg.path.open())`` if that
230 is not sufficient for you.
232 :param header: Case-insensitive header name to retrieve.
233 :type header: str or bytes
235 :returns: The header value, an empty string if the header is
239 :raises LookupError: if the header is not present.
240 :raises NullPointerError: For unexpected notmuch errors.
241 :raises ObjectDestroyedError: if used after destroyed.
243 # The returned is supposedly guaranteed to be UTF-8. Header
244 # names must be ASCII as per RFC x822.
245 if isinstance(name, str):
246 name = name.encode('ascii')
247 ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
248 if ret == capi.ffi.NULL:
249 raise errors.NullPointerError()
250 hdr = capi.ffi.string(ret)
253 return hdr.decode(encoding='utf-8')
257 """The tags associated with the message.
259 This behaves as a set. But removing and adding items to the
260 set removes and adds them to the message in the database.
262 :raises ReadOnlyDatabaseError: When manipulating tags on a
263 database opened in read-only mode.
264 :raises ObjectDestroyedError: if used after destroyed.
267 ref = self._cached_tagset
268 except AttributeError:
273 tagset = tags.MutableTagSet(
274 self, '_msg_p', capi.lib.notmuch_message_get_tags)
275 self._cached_tagset = weakref.ref(tagset)
278 @contextlib.contextmanager
280 """Context manager to freeze the message state.
282 This allows you to perform atomic tag updates::
288 Using This would ensure the message never ends up with no tags
291 It is safe to nest calls to this context manager.
293 :raises ReadOnlyDatabaseError: if the database is opened in
295 :raises UnbalancedFreezeThawError: if you somehow managed to
296 call __exit__ of this context manager more than once. Why
298 :raises ObjectDestroyedError: if used after destroyed.
300 ret = capi.lib.notmuch_message_freeze(self._msg_p)
301 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
302 raise errors.NotmuchError(ret)
307 # Only way to "rollback" these changes is to destroy
308 # ourselves and re-create. Behold.
309 msgid = self.messageid
311 with contextlib.suppress(Exception):
312 new = self._db.find(msgid)
313 self._msg_p = new._msg_p
318 ret = capi.lib.notmuch_message_thaw(self._msg_p)
319 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
320 raise errors.NotmuchError(ret)
324 def properties(self):
325 """A map of arbitrary key-value pairs associated with the message.
327 Be aware that properties may be used by other extensions to
328 store state in. So delete or modify with care.
330 The properties map is somewhat special. It is essentially a
331 multimap-like structure where each key can have multiple
332 values. Therefore accessing a single item using
333 :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
334 will only return you the *first* item if there are multiple
335 and thus are only recommended if you know there to be only one
338 Instead the map has an additional :meth:`PropertiesMap.all`
339 method which can be used to retrieve all properties of a given
340 key. This method also allows iterating of a a subset of the
341 keys starting with a given prefix.
344 ref = self._cached_props
345 except AttributeError:
350 props = PropertiesMap(self, '_msg_p')
351 self._cached_props = weakref.ref(props)
355 """Return an iterator of all replies to this message.
357 This method will only work if the message was created from a
358 thread. Otherwise it will yield no results.
360 :returns: An iterator yielding :class:`Message` instances.
363 # The notmuch_messages_valid call accepts NULL and this will
364 # become an empty iterator, raising StopIteration immediately.
365 # Hence no return value checking here.
366 msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
367 return MessageIter(self, msgs_p, db=self._db)
370 return hash(self.messageid)
372 def __eq__(self, other):
373 if isinstance(other, self.__class__):
374 return self.messageid == other.messageid
377 class OwnedMessage(Message):
378 """An email message owned by parent thread object.
380 This subclass of Message is used for messages that are retrieved
381 from the notmuch database via a parent :class:`notmuch2.Thread`
382 object, which "owns" this message. This means that when this
383 message object is destroyed, by calling :func:`del` or
384 :meth:`_destroy` directly or indirectly, the message is not freed
385 in the notmuch API and the parent :class:`notmuch2.Thread` object
386 can return the same object again when needed.
391 return self._parent.alive
397 class FilenamesIter(base.NotmuchIter):
398 """Iterator for binary filenames objects."""
400 def __init__(self, parent, iter_p):
401 super().__init__(parent, iter_p,
402 fn_destroy=capi.lib.notmuch_filenames_destroy,
403 fn_valid=capi.lib.notmuch_filenames_valid,
404 fn_get=capi.lib.notmuch_filenames_get,
405 fn_next=capi.lib.notmuch_filenames_move_to_next)
408 fname = super().__next__()
409 return capi.ffi.string(fname)
412 class PathIter(FilenamesIter):
413 """Iterator for pathlib.Path objects."""
416 fname = super().__next__()
417 return pathlib.Path(os.fsdecode(fname))
420 class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
421 """A mutable mapping to manage properties.
423 Both keys and values of properties are supposed to be UTF-8
424 strings in libnotmuch. However since the uderlying API uses
425 bytestrings you can use either str or bytes to represent keys and
426 all returned keys and values use :class:`BinString`.
428 Also be aware that ``iter(this_map)`` will return duplicate keys,
429 while the :class:`collections.abc.KeysView` returned by
430 :meth:`keys` is a :class:`collections.abc.Set` subclass. This
431 means the former will yield duplicate keys while the latter won't.
432 It also means ``len(list(iter(this_map)))`` could be different
433 than ``len(this_map.keys())``. ``len(this_map)`` will correspond
434 with the length of the default iterator.
436 Be aware that libnotmuch exposes all of this as iterators, so
437 quite a few operations have O(n) performance instead of the usual
440 Property = collections.namedtuple('Property', ['key', 'value'])
443 def __init__(self, msg, ptr_name):
445 self._ptr = lambda: getattr(msg, ptr_name)
449 if not self._msg.alive:
453 except errors.ObjectDestroyedError:
462 """Return an iterator which iterates over the keys.
464 Be aware that a single key may have multiple values associated
465 with it, if so it will appear multiple times here.
467 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
468 return PropertiesKeyIter(self, iter_p)
471 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
472 it = base.NotmuchIter(
474 fn_destroy=capi.lib.notmuch_message_properties_destroy,
475 fn_valid=capi.lib.notmuch_message_properties_valid,
476 fn_get=capi.lib.notmuch_message_properties_key,
477 fn_next=capi.lib.notmuch_message_properties_move_to_next,
481 def __getitem__(self, key):
482 """Return **the first** peroperty associated with a key."""
483 if isinstance(key, str):
484 key = key.encode('utf-8')
485 value_pp = capi.ffi.new('char**')
486 ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
487 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
488 raise errors.NotmuchError(ret)
489 if value_pp[0] == capi.ffi.NULL:
491 return base.BinString.from_cffi(value_pp[0])
494 """Return a :class:`collections.abc.KeysView` for this map.
496 Even when keys occur multiple times this is a subset of set()
497 so will only contain them once.
499 return collections.abc.KeysView({k: None for k in self})
502 """Return a :class:`collections.abc.ItemsView` for this map.
504 The ItemsView treats a ``(key, value)`` pair as unique, so
505 dupcliate ``(key, value)`` pairs will be merged together.
506 However duplicate keys with different values will be returned.
509 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
510 while capi.lib.notmuch_message_properties_valid(props_p):
511 key = capi.lib.notmuch_message_properties_key(props_p)
512 value = capi.lib.notmuch_message_properties_value(props_p)
513 items.add((base.BinString.from_cffi(key),
514 base.BinString.from_cffi(value)))
515 capi.lib.notmuch_message_properties_move_to_next(props_p)
516 capi.lib.notmuch_message_properties_destroy(props_p)
517 return PropertiesItemsView(items)
520 """Return a :class:`collecions.abc.ValuesView` for this map.
522 All unique property values are included in the view.
525 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
526 while capi.lib.notmuch_message_properties_valid(props_p):
527 value = capi.lib.notmuch_message_properties_value(props_p)
528 values.add(base.BinString.from_cffi(value))
529 capi.lib.notmuch_message_properties_move_to_next(props_p)
530 capi.lib.notmuch_message_properties_destroy(props_p)
531 return PropertiesValuesView(values)
533 def __setitem__(self, key, value):
534 """Add a key-value pair to the properties.
536 You may prefer to use :meth:`add` for clarity since this
537 method usually implies implicit overwriting of an existing key
538 if it exists, while for properties this is not the case.
542 def add(self, key, value):
543 """Add a key-value pair to the properties."""
544 if isinstance(key, str):
545 key = key.encode('utf-8')
546 if isinstance(value, str):
547 value = value.encode('utf-8')
548 ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
549 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
550 raise errors.NotmuchError(ret)
552 def __delitem__(self, key):
553 """Remove all properties with this key."""
554 if isinstance(key, str):
555 key = key.encode('utf-8')
556 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
557 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
558 raise errors.NotmuchError(ret)
560 def remove(self, key, value):
561 """Remove a key-value pair from the properties."""
562 if isinstance(key, str):
563 key = key.encode('utf-8')
564 if isinstance(value, str):
565 value = value.encode('utf-8')
566 ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
567 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
568 raise errors.NotmuchError(ret)
570 def pop(self, key, default=_marker):
574 if default is self._marker:
579 self.remove(key, value)
584 key = next(iter(self))
585 except StopIteration:
587 value = self.pop(key)
591 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
593 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
594 raise errors.NotmuchError(ret)
596 def getall(self, prefix='', *, exact=False):
597 """Return an iterator yielding all properties for a given key prefix.
599 The returned iterator yields all peroperties which start with
600 a given key prefix as ``(key, value)`` namedtuples. If called
601 with ``exact=True`` then only properties which exactly match
602 the prefix are returned, those a key longer than the prefix
603 will not be included.
605 :param prefix: The prefix of the key.
607 if isinstance(prefix, str):
608 prefix = prefix.encode('utf-8')
609 props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
611 return PropertiesIter(self, props_p)
614 class PropertiesKeyIter(base.NotmuchIter):
616 def __init__(self, parent, iter_p):
620 fn_destroy=capi.lib.notmuch_message_properties_destroy,
621 fn_valid=capi.lib.notmuch_message_properties_valid,
622 fn_get=capi.lib.notmuch_message_properties_key,
623 fn_next=capi.lib.notmuch_message_properties_move_to_next)
626 item = super().__next__()
627 return base.BinString.from_cffi(item)
630 class PropertiesIter(base.NotmuchIter):
632 def __init__(self, parent, iter_p):
636 fn_destroy=capi.lib.notmuch_message_properties_destroy,
637 fn_valid=capi.lib.notmuch_message_properties_valid,
638 fn_get=capi.lib.notmuch_message_properties_key,
639 fn_next=capi.lib.notmuch_message_properties_move_to_next,
643 if not self._fn_valid(self._iter_p):
646 key = capi.lib.notmuch_message_properties_key(self._iter_p)
647 value = capi.lib.notmuch_message_properties_value(self._iter_p)
648 capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
649 return PropertiesMap.Property(base.BinString.from_cffi(key),
650 base.BinString.from_cffi(value))
653 class PropertiesItemsView(collections.abc.Set):
655 __slots__ = ('_items',)
657 def __init__(self, items):
661 def _from_iterable(self, it):
665 return len(self._items)
667 def __contains__(self, item):
668 return item in self._items
671 yield from self._items
674 collections.abc.ItemsView.register(PropertiesItemsView)
677 class PropertiesValuesView(collections.abc.Set):
679 __slots__ = ('_values',)
681 def __init__(self, values):
682 self._values = values
685 return len(self._values)
687 def __contains__(self, value):
688 return value in self._values
691 yield from self._values
694 collections.abc.ValuesView.register(PropertiesValuesView)
697 class MessageIter(base.NotmuchIter):
699 def __init__(self, parent, msgs_p, *, db, msg_cls=Message):
701 self._msg_cls = msg_cls
702 super().__init__(parent, msgs_p,
703 fn_destroy=capi.lib.notmuch_messages_destroy,
704 fn_valid=capi.lib.notmuch_messages_valid,
705 fn_get=capi.lib.notmuch_messages_get,
706 fn_next=capi.lib.notmuch_messages_move_to_next)
709 msg_p = super().__next__()
710 return self._msg_cls(self, msg_p, db=self._db)