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.
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 if not self._parent.alive:
68 except errors.ObjectDestroyedError:
78 capi.lib.notmuch_message_destroy(self._msg_p)
83 """The message ID as a string.
85 The message ID is decoded with the ignore error handler. This
86 is fine as long as the message ID is well formed. If it is
87 not valid ASCII then this will be lossy. So if you need to be
88 able to write the exact same message ID back you should use
91 Note that notmuch will decode the message ID value and thus
92 strip off the surrounding ``<`` and ``>`` characters. This is
93 different from Python's :mod:`email` package behaviour which
94 leaves these characters in place.
96 :returns: The message ID.
97 :rtype: :class:`BinString`, this is a normal str but calling
98 bytes() on it will return the original bytes used to create
101 :raises ObjectDestroyedError: if used after destoryed.
103 ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
104 return base.BinString(capi.ffi.string(ret))
110 The thread ID is decoded with the surrogateescape error
111 handler so that it is possible to reconstruct the original
112 thread ID if it is not valid UTF-8.
114 :returns: The thread ID.
115 :rtype: :class:`BinString`, this is a normal str but calling
116 bytes() on it will return the original bytes used to create
119 :raises ObjectDestroyedError: if used after destoryed.
121 ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
122 return base.BinString(capi.ffi.string(ret))
126 """A pathname of the message as a pathlib.Path instance.
128 If multiple files in the database contain the same message ID
129 this will be just one of the files, chosen at random.
131 :raises ObjectDestroyedError: if used after destoryed.
133 ret = capi.lib.notmuch_message_get_filename(self._msg_p)
134 return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
138 """A pathname of the message as a bytes object.
140 See :attr:`path` for details, this is the same but does return
141 the path as a bytes object which is faster but less convenient.
143 :raises ObjectDestroyedError: if used after destoryed.
145 ret = capi.lib.notmuch_message_get_filename(self._msg_p)
146 return capi.ffi.string(ret)
149 """Return an iterator of all files for this message.
151 If multiple files contained the same message ID they will all
152 be returned here. The files are returned as intances of
153 :class:`pathlib.Path`.
155 :returns: Iterator yielding :class:`pathlib.Path` instances.
158 :raises ObjectDestroyedError: if used after destoryed.
160 fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
161 return PathIter(self, fnames_p)
163 def filenamesb(self):
164 """Return an iterator of all files for this message.
166 This is like :meth:`pathnames` but the files are returned as
167 byte objects instead.
169 :returns: Iterator yielding :class:`bytes` instances.
172 :raises ObjectDestroyedError: if used after destoryed.
174 fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
175 return FilenamesIter(self, fnames_p)
179 """Indicates whether this message is a ghost message.
181 A ghost message if a message which we know exists, but it has
182 no files or content associated with it. This can happen if
183 it was referenced by some other message. Only the
184 :attr:`messageid` and :attr:`threadid` attributes are valid
187 :raises ObjectDestroyedError: if used after destoryed.
189 ret = capi.lib.notmuch_message_get_flag(
190 self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
195 """Indicates whether this message was excluded from the query.
197 When a message is created from a search, sometimes messages
198 that where excluded by the search query could still be
199 returned by it, e.g. because they are part of a thread
200 matching the query. the :meth:`Database.query` method allows
201 these messages to be flagged, which results in this property
204 :raises ObjectDestroyedError: if used after destoryed.
206 ret = capi.lib.notmuch_message_get_flag(
207 self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
212 """The message date as an integer.
214 The time the message was sent as an integer number of seconds
215 since the *epoch*, 1 Jan 1970. This is derived from the
216 message's header, you can get the original header value with
219 :raises ObjectDestroyedError: if used after destoryed.
221 return capi.lib.notmuch_message_get_date(self._msg_p)
223 def header(self, name):
224 """Return the value of the named header.
226 Returns the header from notmuch, some common headers are
227 stored in the database, others are read from the file.
228 Headers are returned with their newlines stripped and
229 collapsed concatenated together if they occur multiple times.
230 You may be better off using the standard library email
231 package's ``email.message_from_file(msg.path.open())`` if that
232 is not sufficient for you.
234 :param header: Case-insensitive header name to retrieve.
235 :type header: str or bytes
237 :returns: The header value, an empty string if the header is
241 :raises LookupError: if the header is not present.
242 :raises NullPointerError: For unexpected notmuch errors.
243 :raises ObjectDestroyedError: if used after destoryed.
245 # The returned is supposedly guaranteed to be UTF-8. Header
246 # names must be ASCII as per RFC x822.
247 if isinstance(name, str):
248 name = name.encode('ascii')
249 ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
250 if ret == capi.ffi.NULL:
251 raise errors.NullPointerError()
252 hdr = capi.ffi.string(ret)
255 return hdr.decode(encoding='utf-8')
259 """The tags associated with the message.
261 This behaves as a set. But removing and adding items to the
262 set removes and adds them to the message in the database.
264 :raises ReadOnlyDatabaseError: When manipulating tags on a
265 database opened in read-only mode.
266 :raises ObjectDestroyedError: if used after destoryed.
269 ref = self._cached_tagset
270 except AttributeError:
275 tagset = tags.MutableTagSet(
276 self, '_msg_p', capi.lib.notmuch_message_get_tags)
277 self._cached_tagset = weakref.ref(tagset)
280 @contextlib.contextmanager
282 """Context manager to freeze the message state.
284 This allows you to perform atomic tag updates::
290 Using This would ensure the message never ends up with no tags
293 It is safe to nest calls to this context manager.
295 :raises ReadOnlyDatabaseError: if the database is opened in
297 :raises UnbalancedFreezeThawError: if you somehow managed to
298 call __exit__ of this context manager more than once. Why
300 :raises ObjectDestroyedError: if used after destoryed.
302 ret = capi.lib.notmuch_message_freeze(self._msg_p)
303 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
304 raise errors.NotmuchError(ret)
309 # Only way to "rollback" these changes is to destroy
310 # ourselves and re-create. Behold.
311 msgid = self.messageid
313 with contextlib.suppress(Exception):
314 new = self._db.find(msgid)
315 self._msg_p = new._msg_p
320 ret = capi.lib.notmuch_message_thaw(self._msg_p)
321 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
322 raise errors.NotmuchError(ret)
326 def properties(self):
327 """A map of arbitrary key-value pairs associated with the message.
329 Be aware that properties may be used by other extensions to
330 store state in. So delete or modify with care.
332 The properties map is somewhat special. It is essentially a
333 multimap-like structure where each key can have multiple
334 values. Therefore accessing a single item using
335 :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
336 will only return you the *first* item if there are multiple
337 and thus are only recommended if you know there to be only one
340 Instead the map has an additional :meth:`PropertiesMap.all`
341 method which can be used to retrieve all properties of a given
342 key. This method also allows iterating of a a subset of the
343 keys starting with a given prefix.
346 ref = self._cached_props
347 except AttributeError:
352 props = PropertiesMap(self, '_msg_p')
353 self._cached_props = weakref.ref(props)
357 """Return an iterator of all replies to this message.
359 This method will only work if the message was created from a
360 thread. Otherwise it will yield no results.
362 :returns: An iterator yielding :class:`Message` instances.
365 # The notmuch_messages_valid call accepts NULL and this will
366 # become an empty iterator, raising StopIteration immediately.
367 # Hence no return value checking here.
368 msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
369 return MessageIter(self, msgs_p, db=self._db)
372 return hash(self.messageid)
374 def __eq__(self, other):
375 if isinstance(other, self.__class__):
376 return self.messageid == other.messageid
379 class FilenamesIter(base.NotmuchIter):
380 """Iterator for binary filenames objects."""
382 def __init__(self, parent, iter_p):
383 super().__init__(parent, iter_p,
384 fn_destroy=capi.lib.notmuch_filenames_destroy,
385 fn_valid=capi.lib.notmuch_filenames_valid,
386 fn_get=capi.lib.notmuch_filenames_get,
387 fn_next=capi.lib.notmuch_filenames_move_to_next)
390 fname = super().__next__()
391 return capi.ffi.string(fname)
394 class PathIter(FilenamesIter):
395 """Iterator for pathlib.Path objects."""
398 fname = super().__next__()
399 return pathlib.Path(os.fsdecode(fname))
402 class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
403 """A mutable mapping to manage properties.
405 Both keys and values of properties are supposed to be UTF-8
406 strings in libnotmuch. However since the uderlying API uses
407 bytestrings you can use either str or bytes to represent keys and
408 all returned keys and values use :class:`BinString`.
410 Also be aware that ``iter(this_map)`` will return duplicate keys,
411 while the :class:`collections.abc.KeysView` returned by
412 :meth:`keys` is a :class:`collections.abc.Set` subclass. This
413 means the former will yield duplicate keys while the latter won't.
414 It also means ``len(list(iter(this_map)))`` could be different
415 than ``len(this_map.keys())``. ``len(this_map)`` will correspond
416 with the lenght of the default iterator.
418 Be aware that libnotmuch exposes all of this as iterators, so
419 quite a few operations have O(n) performance instead of the usual
422 Property = collections.namedtuple('Property', ['key', 'value'])
425 def __init__(self, msg, ptr_name):
427 self._ptr = lambda: getattr(msg, ptr_name)
431 if not self._msg.alive:
435 except errors.ObjectDestroyedError:
444 """Return an iterator which iterates over the keys.
446 Be aware that a single key may have multiple values associated
447 with it, if so it will appear multiple times here.
449 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
450 return PropertiesKeyIter(self, iter_p)
453 iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
454 it = base.NotmuchIter(
456 fn_destroy=capi.lib.notmuch_message_properties_destroy,
457 fn_valid=capi.lib.notmuch_message_properties_valid,
458 fn_get=capi.lib.notmuch_message_properties_key,
459 fn_next=capi.lib.notmuch_message_properties_move_to_next,
463 def __getitem__(self, key):
464 """Return **the first** peroperty associated with a key."""
465 if isinstance(key, str):
466 key = key.encode('utf-8')
467 value_pp = capi.ffi.new('char**')
468 ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
469 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
470 raise errors.NotmuchError(ret)
471 if value_pp[0] == capi.ffi.NULL:
473 return base.BinString.from_cffi(value_pp[0])
476 """Return a :class:`collections.abc.KeysView` for this map.
478 Even when keys occur multiple times this is a subset of set()
479 so will only contain them once.
481 return collections.abc.KeysView({k: None for k in self})
484 """Return a :class:`collections.abc.ItemsView` for this map.
486 The ItemsView treats a ``(key, value)`` pair as unique, so
487 dupcliate ``(key, value)`` pairs will be merged together.
488 However duplicate keys with different values will be returned.
491 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
492 while capi.lib.notmuch_message_properties_valid(props_p):
493 key = capi.lib.notmuch_message_properties_key(props_p)
494 value = capi.lib.notmuch_message_properties_value(props_p)
495 items.add((base.BinString.from_cffi(key),
496 base.BinString.from_cffi(value)))
497 capi.lib.notmuch_message_properties_move_to_next(props_p)
498 capi.lib.notmuch_message_properties_destroy(props_p)
499 return PropertiesItemsView(items)
502 """Return a :class:`collecions.abc.ValuesView` for this map.
504 All unique property values are included in the view.
507 props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
508 while capi.lib.notmuch_message_properties_valid(props_p):
509 value = capi.lib.notmuch_message_properties_value(props_p)
510 values.add(base.BinString.from_cffi(value))
511 capi.lib.notmuch_message_properties_move_to_next(props_p)
512 capi.lib.notmuch_message_properties_destroy(props_p)
513 return PropertiesValuesView(values)
515 def __setitem__(self, key, value):
516 """Add a key-value pair to the properties.
518 You may prefer to use :meth:`add` for clarity since this
519 method usually implies implicit overwriting of an existing key
520 if it exists, while for properties this is not the case.
524 def add(self, key, value):
525 """Add a key-value pair to the properties."""
526 if isinstance(key, str):
527 key = key.encode('utf-8')
528 if isinstance(value, str):
529 value = value.encode('utf-8')
530 ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
531 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
532 raise errors.NotmuchError(ret)
534 def __delitem__(self, key):
535 """Remove all properties with this key."""
536 if isinstance(key, str):
537 key = key.encode('utf-8')
538 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
539 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
540 raise errors.NotmuchError(ret)
542 def remove(self, key, value):
543 """Remove a key-value pair from 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_remove_property(self._ptr(), key, value)
549 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
550 raise errors.NotmuchError(ret)
552 def pop(self, key, default=_marker):
556 if default is self._marker:
561 self.remove(key, value)
566 key = next(iter(self))
567 except StopIteration:
569 value = self.pop(key)
573 ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
575 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
576 raise errors.NotmuchError(ret)
578 def getall(self, prefix='', *, exact=False):
579 """Return an iterator yielding all properties for a given key prefix.
581 The returned iterator yields all peroperties which start with
582 a given key prefix as ``(key, value)`` namedtuples. If called
583 with ``exact=True`` then only properties which exactly match
584 the prefix are returned, those a key longer than the prefix
585 will not be included.
587 :param prefix: The prefix of the key.
589 if isinstance(prefix, str):
590 prefix = prefix.encode('utf-8')
591 props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
593 return PropertiesIter(self, props_p)
596 class PropertiesKeyIter(base.NotmuchIter):
598 def __init__(self, parent, iter_p):
602 fn_destroy=capi.lib.notmuch_message_properties_destroy,
603 fn_valid=capi.lib.notmuch_message_properties_valid,
604 fn_get=capi.lib.notmuch_message_properties_key,
605 fn_next=capi.lib.notmuch_message_properties_move_to_next)
608 item = super().__next__()
609 return base.BinString.from_cffi(item)
612 class PropertiesIter(base.NotmuchIter):
614 def __init__(self, parent, iter_p):
618 fn_destroy=capi.lib.notmuch_message_properties_destroy,
619 fn_valid=capi.lib.notmuch_message_properties_valid,
620 fn_get=capi.lib.notmuch_message_properties_key,
621 fn_next=capi.lib.notmuch_message_properties_move_to_next,
625 if not self._fn_valid(self._iter_p):
628 key = capi.lib.notmuch_message_properties_key(self._iter_p)
629 value = capi.lib.notmuch_message_properties_value(self._iter_p)
630 capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
631 return PropertiesMap.Property(base.BinString.from_cffi(key),
632 base.BinString.from_cffi(value))
635 class PropertiesItemsView(collections.abc.Set):
637 __slots__ = ('_items',)
639 def __init__(self, items):
643 def _from_iterable(self, it):
647 return len(self._items)
649 def __contains__(self, item):
650 return item in self._items
653 yield from self._items
656 collections.abc.ItemsView.register(PropertiesItemsView)
659 class PropertiesValuesView(collections.abc.Set):
661 __slots__ = ('_values',)
663 def __init__(self, values):
664 self._values = values
667 return len(self._values)
669 def __contains__(self, value):
670 return value in self._values
673 yield from self._values
676 collections.abc.ValuesView.register(PropertiesValuesView)
679 class MessageIter(base.NotmuchIter):
681 def __init__(self, parent, msgs_p, *, db):
683 super().__init__(parent, msgs_p,
684 fn_destroy=capi.lib.notmuch_messages_destroy,
685 fn_valid=capi.lib.notmuch_messages_valid,
686 fn_get=capi.lib.notmuch_messages_get,
687 fn_next=capi.lib.notmuch_messages_move_to_next)
690 msg_p = super().__next__()
691 return Message(self, msg_p, db=self._db)