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``.
51 :param dup: Whether the message was a duplicate on insertion.
53 :type dup: None or bool
55 _msg_p = base.MemoryPointer()
57 def __init__(self, parent, msg_p, *, db):
64 return self._parent.alive
71 """The message ID as a string.
73 The message ID is decoded with the ignore error handler. This
74 is fine as long as the message ID is well formed. If it is
75 not valid ASCII then this will be lossy. So if you need to be
76 able to write the exact same message ID back you should use
79 Note that notmuch will decode the message ID value and thus
80 strip off the surrounding ``<`` and ``>`` characters. This is
81 different from Python's :mod:`email` package behaviour which
82 leaves these characters in place.
84 :returns: The message ID.
85 :rtype: :class:`BinString`, this is a normal str but calling
86 bytes() on it will return the original bytes used to create
89 :raises ObjectDestroyedError: if used after destroyed.
91 ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
92 return base.BinString(capi.ffi.string(ret))
98 The thread ID is decoded with the surrogateescape error
99 handler so that it is possible to reconstruct the original
100 thread ID if it is not valid UTF-8.
102 :returns: The thread ID.
103 :rtype: :class:`BinString`, this is a normal str but calling
104 bytes() on it will return the original bytes used to create
107 :raises ObjectDestroyedError: if used after destroyed.
109 ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
110 return base.BinString(capi.ffi.string(ret))
114 """A pathname of the message as a pathlib.Path instance.
116 If multiple files in the database contain the same message ID
117 this will be just one of the files, chosen at random.
119 :raises ObjectDestroyedError: if used after destroyed.
121 ret = capi.lib.notmuch_message_get_filename(self._msg_p)
122 return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
126 """A pathname of the message as a bytes object.
128 See :attr:`path` for details, this is the same but does return
129 the path as a bytes object which is faster but less convenient.
131 :raises ObjectDestroyedError: if used after destroyed.
133 ret = capi.lib.notmuch_message_get_filename(self._msg_p)
134 return capi.ffi.string(ret)
137 """Return an iterator of all files for this message.
139 If multiple files contained the same message ID they will all
140 be returned here. The files are returned as intances of
141 :class:`pathlib.Path`.
143 :returns: Iterator yielding :class:`pathlib.Path` instances.
146 :raises ObjectDestroyedError: if used after destroyed.
148 fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
149 return PathIter(self, fnames_p)
151 def filenamesb(self):
152 """Return an iterator of all files for this message.
154 This is like :meth:`pathnames` but the files are returned as
155 byte objects instead.
157 :returns: Iterator yielding :class:`bytes` instances.
160 :raises ObjectDestroyedError: if used after destroyed.
162 fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
163 return FilenamesIter(self, fnames_p)
167 """Indicates whether this message is a ghost message.
169 A ghost message if a message which we know exists, but it has
170 no files or content associated with it. This can happen if
171 it was referenced by some other message. Only the
172 :attr:`messageid` and :attr:`threadid` attributes are valid
175 :raises ObjectDestroyedError: if used after destroyed.
177 ret = capi.lib.notmuch_message_get_flag(
178 self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
183 """Indicates whether this message was excluded from the query.
185 When a message is created from a search, sometimes messages
186 that where excluded by the search query could still be
187 returned by it, e.g. because they are part of a thread
188 matching the query. the :meth:`Database.query` method allows
189 these messages to be flagged, which results in this property
192 :raises ObjectDestroyedError: if used after destroyed.
194 ret = capi.lib.notmuch_message_get_flag(
195 self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
200 """The message date as an integer.
202 The time the message was sent as an integer number of seconds
203 since the *epoch*, 1 Jan 1970. This is derived from the
204 message's header, you can get the original header value with
207 :raises ObjectDestroyedError: if used after destroyed.
209 return capi.lib.notmuch_message_get_date(self._msg_p)
211 def header(self, name):
212 """Return the value of the named header.
214 Returns the header from notmuch, some common headers are
215 stored in the database, others are read from the file.
216 Headers are returned with their newlines stripped and
217 collapsed concatenated together if they occur multiple times.
218 You may be better off using the standard library email
219 package's ``email.message_from_file(msg.path.open())`` if that
220 is not sufficient for you.
222 :param header: Case-insensitive header name to retrieve.
223 :type header: str or bytes
225 :returns: The header value, an empty string if the header is
229 :raises LookupError: if the header is not present.
230 :raises NullPointerError: For unexpected notmuch errors.
231 :raises ObjectDestroyedError: if used after destroyed.
233 # The returned is supposedly guaranteed to be UTF-8. Header
234 # names must be ASCII as per RFC x822.
235 if isinstance(name, str):
236 name = name.encode('ascii')
237 ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
238 if ret == capi.ffi.NULL:
239 raise errors.NullPointerError()
240 hdr = capi.ffi.string(ret)
243 return hdr.decode(encoding='utf-8')
247 """The tags associated with the message.
249 This behaves as a set. But removing and adding items to the
250 set removes and adds them to the message in the database.
252 :raises ReadOnlyDatabaseError: When manipulating tags on a
253 database opened in read-only mode.
254 :raises ObjectDestroyedError: if used after destroyed.
257 ref = self._cached_tagset
258 except AttributeError:
263 tagset = tags.MutableTagSet(
264 self, '_msg_p', capi.lib.notmuch_message_get_tags)
265 self._cached_tagset = weakref.ref(tagset)
268 @contextlib.contextmanager
270 """Context manager to freeze the message state.
272 This allows you to perform atomic tag updates::
278 Using This would ensure the message never ends up with no tags
281 It is safe to nest calls to this context manager.
283 :raises ReadOnlyDatabaseError: if the database is opened in
285 :raises UnbalancedFreezeThawError: if you somehow managed to
286 call __exit__ of this context manager more than once. Why
288 :raises ObjectDestroyedError: if used after destroyed.
290 ret = capi.lib.notmuch_message_freeze(self._msg_p)
291 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
292 raise errors.NotmuchError(ret)
297 # Only way to "rollback" these changes is to destroy
298 # ourselves and re-create. Behold.
299 msgid = self.messageid
301 with contextlib.suppress(Exception):
302 new = self._db.find(msgid)
303 self._msg_p = new._msg_p
308 ret = capi.lib.notmuch_message_thaw(self._msg_p)
309 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
310 raise errors.NotmuchError(ret)
314 def properties(self):
315 """A map of arbitrary key-value pairs associated with the message.
317 Be aware that properties may be used by other extensions to
318 store state in. So delete or modify with care.
320 The properties map is somewhat special. It is essentially a
321 multimap-like structure where each key can have multiple
322 values. Therefore accessing a single item using
323 :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
324 will only return you the *first* item if there are multiple
325 and thus are only recommended if you know there to be only one
328 Instead the map has an additional :meth:`PropertiesMap.all`
329 method which can be used to retrieve all properties of a given
330 key. This method also allows iterating of a a subset of the
331 keys starting with a given prefix.
334 ref = self._cached_props
335 except AttributeError:
340 props = PropertiesMap(self, '_msg_p')
341 self._cached_props = weakref.ref(props)
345 """Return an iterator of all replies to this message.
347 This method will only work if the message was created from a
348 thread. Otherwise it will yield no results.
350 :returns: An iterator yielding :class:`Message` instances.
353 # The notmuch_messages_valid call accepts NULL and this will
354 # become an empty iterator, raising StopIteration immediately.
355 # Hence no return value checking here.
356 msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
357 return MessageIter(self, msgs_p, db=self._db)
360 return hash(self.messageid)
362 def __eq__(self, other):
363 if isinstance(other, self.__class__):
364 return self.messageid == other.messageid
366 class StandaloneMessage(Message):
367 """An email message stored in the notmuch database.
369 This subclass of Message is used for messages that are retrieved from the
370 database directly and are not owned by a query.
374 if not self._parent.alive:
378 except errors.ObjectDestroyedError:
388 capi.lib.notmuch_message_destroy(self._msg_p)
391 class FilenamesIter(base.NotmuchIter):
392 """Iterator for binary filenames objects."""
394 def __init__(self, parent, iter_p):
395 super().__init__(parent, iter_p,
396 fn_destroy=capi.lib.notmuch_filenames_destroy,
397 fn_valid=capi.lib.notmuch_filenames_valid,
398 fn_get=capi.lib.notmuch_filenames_get,
399 fn_next=capi.lib.notmuch_filenames_move_to_next)
402 fname = super().__next__()
403 return capi.ffi.string(fname)
406 class PathIter(FilenamesIter):
407 """Iterator for pathlib.Path objects."""
410 fname = super().__next__()
411 return pathlib.Path(os.fsdecode(fname))
414 class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
415 """A mutable mapping to manage properties.
417 Both keys and values of properties are supposed to be UTF-8
418 strings in libnotmuch. However since the uderlying API uses
419 bytestrings you can use either str or bytes to represent keys and
420 all returned keys and values use :class:`BinString`.
422 Also be aware that ``iter(this_map)`` will return duplicate keys,
423 while the :class:`collections.abc.KeysView` returned by
424 :meth:`keys` is a :class:`collections.abc.Set` subclass. This
425 means the former will yield duplicate keys while the latter won't.
426 It also means ``len(list(iter(this_map)))`` could be different
427 than ``len(this_map.keys())``. ``len(this_map)`` will correspond
428 with the lenght of the default iterator.
430 Be aware that libnotmuch exposes all of this as iterators, so
431 quite a few operations have O(n) performance instead of the usual
434 Property = collections.namedtuple('Property', ['key', 'value'])
437 def __init__(self, msg, ptr_name):
439 self._ptr = lambda: getattr(msg, ptr_name)
443 if not self._msg.alive:
447 except errors.ObjectDestroyedError:
456 """Return an iterator which iterates over the keys.
458 Be aware that a single key may have multiple values associated
459 with it, if so it will appear multiple times here.
461 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
462 return PropertiesKeyIter(self, iter_p)
465 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
466 it = base.NotmuchIter(
468 fn_destroy=capi.lib.notmuch_message_properties_destroy,
469 fn_valid=capi.lib.notmuch_message_properties_valid,
470 fn_get=capi.lib.notmuch_message_properties_key,
471 fn_next=capi.lib.notmuch_message_properties_move_to_next,
475 def __getitem__(self, key):
476 """Return **the first** peroperty associated with a key."""
477 if isinstance(key, str):
478 key = key.encode('utf-8')
479 value_pp = capi.ffi.new('char**')
480 ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
481 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
482 raise errors.NotmuchError(ret)
483 if value_pp[0] == capi.ffi.NULL:
485 return base.BinString.from_cffi(value_pp[0])
488 """Return a :class:`collections.abc.KeysView` for this map.
490 Even when keys occur multiple times this is a subset of set()
491 so will only contain them once.
493 return collections.abc.KeysView({k: None for k in self})
496 """Return a :class:`collections.abc.ItemsView` for this map.
498 The ItemsView treats a ``(key, value)`` pair as unique, so
499 dupcliate ``(key, value)`` pairs will be merged together.
500 However duplicate keys with different values will be returned.
503 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
504 while capi.lib.notmuch_message_properties_valid(props_p):
505 key = capi.lib.notmuch_message_properties_key(props_p)
506 value = capi.lib.notmuch_message_properties_value(props_p)
507 items.add((base.BinString.from_cffi(key),
508 base.BinString.from_cffi(value)))
509 capi.lib.notmuch_message_properties_move_to_next(props_p)
510 capi.lib.notmuch_message_properties_destroy(props_p)
511 return PropertiesItemsView(items)
514 """Return a :class:`collecions.abc.ValuesView` for this map.
516 All unique property values are included in the view.
519 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
520 while capi.lib.notmuch_message_properties_valid(props_p):
521 value = capi.lib.notmuch_message_properties_value(props_p)
522 values.add(base.BinString.from_cffi(value))
523 capi.lib.notmuch_message_properties_move_to_next(props_p)
524 capi.lib.notmuch_message_properties_destroy(props_p)
525 return PropertiesValuesView(values)
527 def __setitem__(self, key, value):
528 """Add a key-value pair to the properties.
530 You may prefer to use :meth:`add` for clarity since this
531 method usually implies implicit overwriting of an existing key
532 if it exists, while for properties this is not the case.
536 def add(self, key, value):
537 """Add a key-value pair to the properties."""
538 if isinstance(key, str):
539 key = key.encode('utf-8')
540 if isinstance(value, str):
541 value = value.encode('utf-8')
542 ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
543 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
544 raise errors.NotmuchError(ret)
546 def __delitem__(self, key):
547 """Remove all properties with this key."""
548 if isinstance(key, str):
549 key = key.encode('utf-8')
550 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
551 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
552 raise errors.NotmuchError(ret)
554 def remove(self, key, value):
555 """Remove a key-value pair from the properties."""
556 if isinstance(key, str):
557 key = key.encode('utf-8')
558 if isinstance(value, str):
559 value = value.encode('utf-8')
560 ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
561 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
562 raise errors.NotmuchError(ret)
564 def pop(self, key, default=_marker):
568 if default is self._marker:
573 self.remove(key, value)
578 key = next(iter(self))
579 except StopIteration:
581 value = self.pop(key)
585 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
587 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
588 raise errors.NotmuchError(ret)
590 def getall(self, prefix='', *, exact=False):
591 """Return an iterator yielding all properties for a given key prefix.
593 The returned iterator yields all peroperties which start with
594 a given key prefix as ``(key, value)`` namedtuples. If called
595 with ``exact=True`` then only properties which exactly match
596 the prefix are returned, those a key longer than the prefix
597 will not be included.
599 :param prefix: The prefix of the key.
601 if isinstance(prefix, str):
602 prefix = prefix.encode('utf-8')
603 props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
605 return PropertiesIter(self, props_p)
608 class PropertiesKeyIter(base.NotmuchIter):
610 def __init__(self, parent, iter_p):
614 fn_destroy=capi.lib.notmuch_message_properties_destroy,
615 fn_valid=capi.lib.notmuch_message_properties_valid,
616 fn_get=capi.lib.notmuch_message_properties_key,
617 fn_next=capi.lib.notmuch_message_properties_move_to_next)
620 item = super().__next__()
621 return base.BinString.from_cffi(item)
624 class PropertiesIter(base.NotmuchIter):
626 def __init__(self, parent, iter_p):
630 fn_destroy=capi.lib.notmuch_message_properties_destroy,
631 fn_valid=capi.lib.notmuch_message_properties_valid,
632 fn_get=capi.lib.notmuch_message_properties_key,
633 fn_next=capi.lib.notmuch_message_properties_move_to_next,
637 if not self._fn_valid(self._iter_p):
640 key = capi.lib.notmuch_message_properties_key(self._iter_p)
641 value = capi.lib.notmuch_message_properties_value(self._iter_p)
642 capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
643 return PropertiesMap.Property(base.BinString.from_cffi(key),
644 base.BinString.from_cffi(value))
647 class PropertiesItemsView(collections.abc.Set):
649 __slots__ = ('_items',)
651 def __init__(self, items):
655 def _from_iterable(self, it):
659 return len(self._items)
661 def __contains__(self, item):
662 return item in self._items
665 yield from self._items
668 collections.abc.ItemsView.register(PropertiesItemsView)
671 class PropertiesValuesView(collections.abc.Set):
673 __slots__ = ('_values',)
675 def __init__(self, values):
676 self._values = values
679 return len(self._values)
681 def __contains__(self, value):
682 return value in self._values
685 yield from self._values
688 collections.abc.ValuesView.register(PropertiesValuesView)
691 class MessageIter(base.NotmuchIter):
693 def __init__(self, parent, msgs_p, *, db):
695 super().__init__(parent, msgs_p,
696 fn_destroy=capi.lib.notmuch_messages_destroy,
697 fn_valid=capi.lib.notmuch_messages_valid,
698 fn_get=capi.lib.notmuch_messages_get,
699 fn_next=capi.lib.notmuch_messages_move_to_next)
702 msg_p = super().__next__()
703 return Message(self, msg_p, db=self._db)