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 """Indicates whether this message was matched by the query.
212 When a thread is created from a search, some of the
213 messages may not match the original query. This property
214 is set to *True* for those that do match.
216 :raises ObjectDestroyedError: if used after destroyed.
218 ret = capi.lib.notmuch_message_get_flag(
219 self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_MATCH)
224 """The message date as an integer.
226 The time the message was sent as an integer number of seconds
227 since the *epoch*, 1 Jan 1970. This is derived from the
228 message's header, you can get the original header value with
231 :raises ObjectDestroyedError: if used after destroyed.
233 return capi.lib.notmuch_message_get_date(self._msg_p)
235 def header(self, name):
236 """Return the value of the named header.
238 Returns the header from notmuch, some common headers are
239 stored in the database, others are read from the file.
240 Headers are returned with their newlines stripped and
241 collapsed concatenated together if they occur multiple times.
242 You may be better off using the standard library email
243 package's ``email.message_from_file(msg.path.open())`` if that
244 is not sufficient for you.
246 :param header: Case-insensitive header name to retrieve.
247 :type header: str or bytes
249 :returns: The header value, an empty string if the header is
253 :raises LookupError: if the header is not present.
254 :raises NullPointerError: For unexpected notmuch errors.
255 :raises ObjectDestroyedError: if used after destroyed.
257 # The returned is supposedly guaranteed to be UTF-8. Header
258 # names must be ASCII as per RFC x822.
259 if isinstance(name, str):
260 name = name.encode('ascii')
261 ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
262 if ret == capi.ffi.NULL:
263 raise errors.NullPointerError()
264 hdr = capi.ffi.string(ret)
267 return hdr.decode(encoding='utf-8')
271 """The tags associated with the message.
273 This behaves as a set. But removing and adding items to the
274 set removes and adds them to the message in the database.
276 :raises ReadOnlyDatabaseError: When manipulating tags on a
277 database opened in read-only mode.
278 :raises ObjectDestroyedError: if used after destroyed.
281 ref = self._cached_tagset
282 except AttributeError:
287 tagset = tags.MutableTagSet(
288 self, '_msg_p', capi.lib.notmuch_message_get_tags)
289 self._cached_tagset = weakref.ref(tagset)
292 @contextlib.contextmanager
294 """Context manager to freeze the message state.
296 This allows you to perform atomic tag updates::
302 Using This would ensure the message never ends up with no tags
305 It is safe to nest calls to this context manager.
307 :raises ReadOnlyDatabaseError: if the database is opened in
309 :raises UnbalancedFreezeThawError: if you somehow managed to
310 call __exit__ of this context manager more than once. Why
312 :raises ObjectDestroyedError: if used after destroyed.
314 ret = capi.lib.notmuch_message_freeze(self._msg_p)
315 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
316 raise errors.NotmuchError(ret)
321 # Only way to "rollback" these changes is to destroy
322 # ourselves and re-create. Behold.
323 msgid = self.messageid
325 with contextlib.suppress(Exception):
326 new = self._db.find(msgid)
327 self._msg_p = new._msg_p
332 ret = capi.lib.notmuch_message_thaw(self._msg_p)
333 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
334 raise errors.NotmuchError(ret)
338 def properties(self):
339 """A map of arbitrary key-value pairs associated with the message.
341 Be aware that properties may be used by other extensions to
342 store state in. So delete or modify with care.
344 The properties map is somewhat special. It is essentially a
345 multimap-like structure where each key can have multiple
346 values. Therefore accessing a single item using
347 :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
348 will only return you the *first* item if there are multiple
349 and thus are only recommended if you know there to be only one
352 Instead the map has an additional :meth:`PropertiesMap.all`
353 method which can be used to retrieve all properties of a given
354 key. This method also allows iterating of a a subset of the
355 keys starting with a given prefix.
358 ref = self._cached_props
359 except AttributeError:
364 props = PropertiesMap(self, '_msg_p')
365 self._cached_props = weakref.ref(props)
369 """Return an iterator of all replies to this message.
371 This method will only work if the message was created from a
372 thread. Otherwise it will yield no results.
374 :returns: An iterator yielding :class:`OwnedMessage` instances.
377 # The notmuch_messages_valid call accepts NULL and this will
378 # become an empty iterator, raising StopIteration immediately.
379 # Hence no return value checking here.
380 msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
381 return MessageIter(self, msgs_p, db=self._db, msg_cls=OwnedMessage)
384 return hash(self.messageid)
386 def __eq__(self, other):
387 if isinstance(other, self.__class__):
388 return self.messageid == other.messageid
391 class OwnedMessage(Message):
392 """An email message owned by parent thread object.
394 This subclass of Message is used for messages that are retrieved
395 from the notmuch database via a parent :class:`notmuch2.Thread`
396 object, which "owns" this message. This means that when this
397 message object is destroyed, by calling :func:`del` or
398 :meth:`_destroy` directly or indirectly, the message is not freed
399 in the notmuch API and the parent :class:`notmuch2.Thread` object
400 can return the same object again when needed.
405 return self._parent.alive
411 class FilenamesIter(base.NotmuchIter):
412 """Iterator for binary filenames objects."""
414 def __init__(self, parent, iter_p):
415 super().__init__(parent, iter_p,
416 fn_destroy=capi.lib.notmuch_filenames_destroy,
417 fn_valid=capi.lib.notmuch_filenames_valid,
418 fn_get=capi.lib.notmuch_filenames_get,
419 fn_next=capi.lib.notmuch_filenames_move_to_next)
422 fname = super().__next__()
423 return capi.ffi.string(fname)
426 class PathIter(FilenamesIter):
427 """Iterator for pathlib.Path objects."""
430 fname = super().__next__()
431 return pathlib.Path(os.fsdecode(fname))
434 class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
435 """A mutable mapping to manage properties.
437 Both keys and values of properties are supposed to be UTF-8
438 strings in libnotmuch. However since the uderlying API uses
439 bytestrings you can use either str or bytes to represent keys and
440 all returned keys and values use :class:`BinString`.
442 Also be aware that ``iter(this_map)`` will return duplicate keys,
443 while the :class:`collections.abc.KeysView` returned by
444 :meth:`keys` is a :class:`collections.abc.Set` subclass. This
445 means the former will yield duplicate keys while the latter won't.
446 It also means ``len(list(iter(this_map)))`` could be different
447 than ``len(this_map.keys())``. ``len(this_map)`` will correspond
448 with the length of the default iterator.
450 Be aware that libnotmuch exposes all of this as iterators, so
451 quite a few operations have O(n) performance instead of the usual
454 Property = collections.namedtuple('Property', ['key', 'value'])
457 def __init__(self, msg, ptr_name):
459 self._ptr = lambda: getattr(msg, ptr_name)
463 if not self._msg.alive:
467 except errors.ObjectDestroyedError:
476 """Return an iterator which iterates over the keys.
478 Be aware that a single key may have multiple values associated
479 with it, if so it will appear multiple times here.
481 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
482 return PropertiesKeyIter(self, iter_p)
485 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
486 it = base.NotmuchIter(
488 fn_destroy=capi.lib.notmuch_message_properties_destroy,
489 fn_valid=capi.lib.notmuch_message_properties_valid,
490 fn_get=capi.lib.notmuch_message_properties_key,
491 fn_next=capi.lib.notmuch_message_properties_move_to_next,
495 def __getitem__(self, key):
496 """Return **the first** peroperty associated with a key."""
497 if isinstance(key, str):
498 key = key.encode('utf-8')
499 value_pp = capi.ffi.new('char**')
500 ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
501 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
502 raise errors.NotmuchError(ret)
503 if value_pp[0] == capi.ffi.NULL:
505 return base.BinString.from_cffi(value_pp[0])
508 """Return a :class:`collections.abc.KeysView` for this map.
510 Even when keys occur multiple times this is a subset of set()
511 so will only contain them once.
513 return collections.abc.KeysView({k: None for k in self})
516 """Return a :class:`collections.abc.ItemsView` for this map.
518 The ItemsView treats a ``(key, value)`` pair as unique, so
519 dupcliate ``(key, value)`` pairs will be merged together.
520 However duplicate keys with different values will be returned.
523 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
524 while capi.lib.notmuch_message_properties_valid(props_p):
525 key = capi.lib.notmuch_message_properties_key(props_p)
526 value = capi.lib.notmuch_message_properties_value(props_p)
527 items.add((base.BinString.from_cffi(key),
528 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 PropertiesItemsView(items)
534 """Return a :class:`collecions.abc.ValuesView` for this map.
536 All unique property values are included in the view.
539 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
540 while capi.lib.notmuch_message_properties_valid(props_p):
541 value = capi.lib.notmuch_message_properties_value(props_p)
542 values.add(base.BinString.from_cffi(value))
543 capi.lib.notmuch_message_properties_move_to_next(props_p)
544 capi.lib.notmuch_message_properties_destroy(props_p)
545 return PropertiesValuesView(values)
547 def __setitem__(self, key, value):
548 """Add a key-value pair to the properties.
550 You may prefer to use :meth:`add` for clarity since this
551 method usually implies implicit overwriting of an existing key
552 if it exists, while for properties this is not the case.
556 def add(self, key, value):
557 """Add a key-value pair to the properties."""
558 if isinstance(key, str):
559 key = key.encode('utf-8')
560 if isinstance(value, str):
561 value = value.encode('utf-8')
562 ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
563 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
564 raise errors.NotmuchError(ret)
566 def __delitem__(self, key):
567 """Remove all properties with this key."""
568 if isinstance(key, str):
569 key = key.encode('utf-8')
570 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
571 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
572 raise errors.NotmuchError(ret)
574 def remove(self, key, value):
575 """Remove a key-value pair from the properties."""
576 if isinstance(key, str):
577 key = key.encode('utf-8')
578 if isinstance(value, str):
579 value = value.encode('utf-8')
580 ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
581 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
582 raise errors.NotmuchError(ret)
584 def pop(self, key, default=_marker):
588 if default is self._marker:
593 self.remove(key, value)
598 key = next(iter(self))
599 except StopIteration:
601 value = self.pop(key)
605 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
607 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
608 raise errors.NotmuchError(ret)
610 def getall(self, prefix='', *, exact=False):
611 """Return an iterator yielding all properties for a given key prefix.
613 The returned iterator yields all peroperties which start with
614 a given key prefix as ``(key, value)`` namedtuples. If called
615 with ``exact=True`` then only properties which exactly match
616 the prefix are returned, those a key longer than the prefix
617 will not be included.
619 :param prefix: The prefix of the key.
621 if isinstance(prefix, str):
622 prefix = prefix.encode('utf-8')
623 props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
625 return PropertiesIter(self, props_p)
628 class PropertiesKeyIter(base.NotmuchIter):
630 def __init__(self, parent, iter_p):
634 fn_destroy=capi.lib.notmuch_message_properties_destroy,
635 fn_valid=capi.lib.notmuch_message_properties_valid,
636 fn_get=capi.lib.notmuch_message_properties_key,
637 fn_next=capi.lib.notmuch_message_properties_move_to_next)
640 item = super().__next__()
641 return base.BinString.from_cffi(item)
644 class PropertiesIter(base.NotmuchIter):
646 def __init__(self, parent, iter_p):
650 fn_destroy=capi.lib.notmuch_message_properties_destroy,
651 fn_valid=capi.lib.notmuch_message_properties_valid,
652 fn_get=capi.lib.notmuch_message_properties_key,
653 fn_next=capi.lib.notmuch_message_properties_move_to_next,
657 if not self._fn_valid(self._iter_p):
660 key = capi.lib.notmuch_message_properties_key(self._iter_p)
661 value = capi.lib.notmuch_message_properties_value(self._iter_p)
662 capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
663 return PropertiesMap.Property(base.BinString.from_cffi(key),
664 base.BinString.from_cffi(value))
667 class PropertiesItemsView(collections.abc.Set):
669 __slots__ = ('_items',)
671 def __init__(self, items):
675 def _from_iterable(self, it):
679 return len(self._items)
681 def __contains__(self, item):
682 return item in self._items
685 yield from self._items
688 collections.abc.ItemsView.register(PropertiesItemsView)
691 class PropertiesValuesView(collections.abc.Set):
693 __slots__ = ('_values',)
695 def __init__(self, values):
696 self._values = values
699 return len(self._values)
701 def __contains__(self, value):
702 return value in self._values
705 yield from self._values
708 collections.abc.ValuesView.register(PropertiesValuesView)
711 class MessageIter(base.NotmuchIter):
713 def __init__(self, parent, msgs_p, *, db, msg_cls=Message):
715 self._msg_cls = msg_cls
716 super().__init__(parent, msgs_p,
717 fn_destroy=capi.lib.notmuch_messages_destroy,
718 fn_valid=capi.lib.notmuch_messages_valid,
719 fn_get=capi.lib.notmuch_messages_get,
720 fn_next=capi.lib.notmuch_messages_move_to_next)
723 msg_p = super().__next__()
724 return self._msg_cls(self, msg_p, db=self._db)