]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_message.py
python/notmuch2: do not destroy messages owned by a query
[notmuch] / bindings / python-cffi / notmuch2 / _message.py
1 import collections
2 import contextlib
3 import os
4 import pathlib
5 import weakref
6
7 import notmuch2._base as base
8 import notmuch2._capi as capi
9 import notmuch2._errors as errors
10 import notmuch2._tags as tags
11
12
13 __all__ = ['Message']
14
15
16 class Message(base.NotmuchObject):
17     """An email message stored in the notmuch database retrieved via a query.
18
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.
23
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
27     this as such::
28
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)
33
34     Most commonly the functionality provided by notmuch is sufficient
35     to read email however.
36
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
40     duplicates.
41
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.
47     :type db: Database
48     :param msg_p: The C pointer to the ``notmuch_message_t``.
49     :type msg_p: <cdata>
50
51     :param dup: Whether the message was a duplicate on insertion.
52
53     :type dup: None or bool
54     """
55     _msg_p = base.MemoryPointer()
56
57     def __init__(self, parent, msg_p, *, db):
58         self._parent = parent
59         self._msg_p = msg_p
60         self._db = db
61
62     @property
63     def alive(self):
64         return self._parent.alive
65
66     def _destroy(self):
67         pass
68
69     @property
70     def messageid(self):
71         """The message ID as a string.
72
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
77         :attr:`messageidb`.
78
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.
83
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
87            it.
88
89         :raises ObjectDestroyedError: if used after destroyed.
90         """
91         ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
92         return base.BinString(capi.ffi.string(ret))
93
94     @property
95     def threadid(self):
96         """The thread ID.
97
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.
101
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
105            it.
106
107         :raises ObjectDestroyedError: if used after destroyed.
108         """
109         ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
110         return base.BinString(capi.ffi.string(ret))
111
112     @property
113     def path(self):
114         """A pathname of the message as a pathlib.Path instance.
115
116         If multiple files in the database contain the same message ID
117         this will be just one of the files, chosen at random.
118
119         :raises ObjectDestroyedError: if used after destroyed.
120         """
121         ret = capi.lib.notmuch_message_get_filename(self._msg_p)
122         return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
123
124     @property
125     def pathb(self):
126         """A pathname of the message as a bytes object.
127
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.
130
131         :raises ObjectDestroyedError: if used after destroyed.
132         """
133         ret = capi.lib.notmuch_message_get_filename(self._msg_p)
134         return capi.ffi.string(ret)
135
136     def filenames(self):
137         """Return an iterator of all files for this message.
138
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`.
142
143         :returns: Iterator yielding :class:`pathlib.Path` instances.
144         :rtype: iter
145
146         :raises ObjectDestroyedError: if used after destroyed.
147         """
148         fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
149         return PathIter(self, fnames_p)
150
151     def filenamesb(self):
152         """Return an iterator of all files for this message.
153
154         This is like :meth:`pathnames` but the files are returned as
155         byte objects instead.
156
157         :returns: Iterator yielding :class:`bytes` instances.
158         :rtype: iter
159
160         :raises ObjectDestroyedError: if used after destroyed.
161         """
162         fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
163         return FilenamesIter(self, fnames_p)
164
165     @property
166     def ghost(self):
167         """Indicates whether this message is a ghost message.
168
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
173         for it.
174
175         :raises ObjectDestroyedError: if used after destroyed.
176         """
177         ret = capi.lib.notmuch_message_get_flag(
178             self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
179         return bool(ret)
180
181     @property
182     def excluded(self):
183         """Indicates whether this message was excluded from the query.
184
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
190         being set to *True*.
191
192         :raises ObjectDestroyedError: if used after destroyed.
193         """
194         ret = capi.lib.notmuch_message_get_flag(
195             self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
196         return bool(ret)
197
198     @property
199     def date(self):
200         """The message date as an integer.
201
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
205         :meth:`header`.
206
207         :raises ObjectDestroyedError: if used after destroyed.
208         """
209         return capi.lib.notmuch_message_get_date(self._msg_p)
210
211     def header(self, name):
212         """Return the value of the named header.
213
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.
221
222         :param header: Case-insensitive header name to retrieve.
223         :type header: str or bytes
224
225         :returns: The header value, an empty string if the header is
226            not present.
227         :rtype: str
228
229         :raises LookupError: if the header is not present.
230         :raises NullPointerError: For unexpected notmuch errors.
231         :raises ObjectDestroyedError: if used after destroyed.
232         """
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)
241         if not hdr:
242             raise LookupError
243         return hdr.decode(encoding='utf-8')
244
245     @property
246     def tags(self):
247         """The tags associated with the message.
248
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.
251
252         :raises ReadOnlyDatabaseError: When manipulating tags on a
253            database opened in read-only mode.
254         :raises ObjectDestroyedError: if used after destroyed.
255         """
256         try:
257             ref = self._cached_tagset
258         except AttributeError:
259             tagset = None
260         else:
261             tagset = ref()
262         if tagset is None:
263             tagset = tags.MutableTagSet(
264                 self, '_msg_p', capi.lib.notmuch_message_get_tags)
265             self._cached_tagset = weakref.ref(tagset)
266         return tagset
267
268     @contextlib.contextmanager
269     def frozen(self):
270         """Context manager to freeze the message state.
271
272         This allows you to perform atomic tag updates::
273
274            with msg.frozen():
275                msg.tags.clear()
276                msg.tags.add('foo')
277
278         Using This would ensure the message never ends up with no tags
279         applied at all.
280
281         It is safe to nest calls to this context manager.
282
283         :raises ReadOnlyDatabaseError: if the database is opened in
284            read-only mode.
285         :raises UnbalancedFreezeThawError: if you somehow managed to
286            call __exit__ of this context manager more than once.  Why
287            did you do that?
288         :raises ObjectDestroyedError: if used after destroyed.
289         """
290         ret = capi.lib.notmuch_message_freeze(self._msg_p)
291         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
292             raise errors.NotmuchError(ret)
293         self._frozen = True
294         try:
295             yield
296         except Exception:
297             # Only way to "rollback" these changes is to destroy
298             # ourselves and re-create.  Behold.
299             msgid = self.messageid
300             self._destroy()
301             with contextlib.suppress(Exception):
302                 new = self._db.find(msgid)
303                 self._msg_p = new._msg_p
304                 new._msg_p = None
305                 del new
306             raise
307         else:
308             ret = capi.lib.notmuch_message_thaw(self._msg_p)
309             if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
310                 raise errors.NotmuchError(ret)
311             self._frozen = False
312
313     @property
314     def properties(self):
315         """A map of arbitrary key-value pairs associated with the message.
316
317         Be aware that properties may be used by other extensions to
318         store state in.  So delete or modify with care.
319
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
326         value.
327
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.
332         """
333         try:
334             ref = self._cached_props
335         except AttributeError:
336             props = None
337         else:
338             props = ref()
339         if props is None:
340             props = PropertiesMap(self, '_msg_p')
341             self._cached_props = weakref.ref(props)
342         return props
343
344     def replies(self):
345         """Return an iterator of all replies to this message.
346
347         This method will only work if the message was created from a
348         thread.  Otherwise it will yield no results.
349
350         :returns: An iterator yielding :class:`Message` instances.
351         :rtype: MessageIter
352         """
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)
358
359     def __hash__(self):
360         return hash(self.messageid)
361
362     def __eq__(self, other):
363         if isinstance(other, self.__class__):
364             return self.messageid == other.messageid
365
366 class StandaloneMessage(Message):
367     """An email message stored in the notmuch database.
368
369     This subclass of Message is used for messages that are retrieved from the
370     database directly and are not owned by a query.
371     """
372     @property
373     def alive(self):
374         if not self._parent.alive:
375             return False
376         try:
377             self._msg_p
378         except errors.ObjectDestroyedError:
379             return False
380         else:
381             return True
382
383     def __del__(self):
384         self._destroy()
385
386     def _destroy(self):
387         if self.alive:
388             capi.lib.notmuch_message_destroy(self._msg_p)
389         self._msg_p = None
390
391 class FilenamesIter(base.NotmuchIter):
392     """Iterator for binary filenames objects."""
393
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)
400
401     def __next__(self):
402         fname = super().__next__()
403         return capi.ffi.string(fname)
404
405
406 class PathIter(FilenamesIter):
407     """Iterator for pathlib.Path objects."""
408
409     def __next__(self):
410         fname = super().__next__()
411         return pathlib.Path(os.fsdecode(fname))
412
413
414 class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
415     """A mutable mapping to manage properties.
416
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`.
421
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.
429
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
432     O(1).
433     """
434     Property = collections.namedtuple('Property', ['key', 'value'])
435     _marker = object()
436
437     def __init__(self, msg, ptr_name):
438         self._msg = msg
439         self._ptr = lambda: getattr(msg, ptr_name)
440
441     @property
442     def alive(self):
443         if not self._msg.alive:
444             return False
445         try:
446             self._ptr
447         except errors.ObjectDestroyedError:
448             return False
449         else:
450             return True
451
452     def _destroy(self):
453         pass
454
455     def __iter__(self):
456         """Return an iterator which iterates over the keys.
457
458         Be aware that a single key may have multiple values associated
459         with it, if so it will appear multiple times here.
460         """
461         iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
462         return PropertiesKeyIter(self, iter_p)
463
464     def __len__(self):
465         iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
466         it = base.NotmuchIter(
467             self, iter_p,
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,
472         )
473         return len(list(it))
474
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:
484             raise KeyError
485         return base.BinString.from_cffi(value_pp[0])
486
487     def keys(self):
488         """Return a :class:`collections.abc.KeysView` for this map.
489
490         Even when keys occur multiple times this is a subset of set()
491         so will only contain them once.
492         """
493         return collections.abc.KeysView({k: None for k in self})
494
495     def items(self):
496         """Return a :class:`collections.abc.ItemsView` for this map.
497
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.
501         """
502         items = set()
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)
512
513     def values(self):
514         """Return a :class:`collecions.abc.ValuesView` for this map.
515
516         All unique property values are included in the view.
517         """
518         values = set()
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)
526
527     def __setitem__(self, key, value):
528         """Add a key-value pair to the properties.
529
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.
533         """
534         self.add(key, value)
535
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)
545
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)
553
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)
563
564     def pop(self, key, default=_marker):
565         try:
566             value = self[key]
567         except KeyError:
568             if default is self._marker:
569                 raise
570             else:
571                 return default
572         else:
573             self.remove(key, value)
574             return value
575
576     def popitem(self):
577         try:
578             key = next(iter(self))
579         except StopIteration:
580             raise KeyError
581         value = self.pop(key)
582         return (key, value)
583
584     def clear(self):
585         ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
586                                                              capi.ffi.NULL)
587         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
588             raise errors.NotmuchError(ret)
589
590     def getall(self, prefix='', *, exact=False):
591         """Return an iterator yielding all properties for a given key prefix.
592
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.
598
599         :param prefix: The prefix of the key.
600         """
601         if isinstance(prefix, str):
602             prefix = prefix.encode('utf-8')
603         props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
604                                                           prefix, exact)
605         return PropertiesIter(self, props_p)
606
607
608 class PropertiesKeyIter(base.NotmuchIter):
609
610     def __init__(self, parent, iter_p):
611         super().__init__(
612             parent,
613             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)
618
619     def __next__(self):
620         item = super().__next__()
621         return base.BinString.from_cffi(item)
622
623
624 class PropertiesIter(base.NotmuchIter):
625
626     def __init__(self, parent, iter_p):
627         super().__init__(
628             parent,
629             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,
634         )
635
636     def __next__(self):
637         if not self._fn_valid(self._iter_p):
638             self._destroy()
639             raise StopIteration
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))
645
646
647 class PropertiesItemsView(collections.abc.Set):
648
649     __slots__ = ('_items',)
650
651     def __init__(self, items):
652         self._items = items
653
654     @classmethod
655     def _from_iterable(self, it):
656         return set(it)
657
658     def __len__(self):
659         return len(self._items)
660
661     def __contains__(self, item):
662         return item in self._items
663
664     def __iter__(self):
665         yield from self._items
666
667
668 collections.abc.ItemsView.register(PropertiesItemsView)
669
670
671 class PropertiesValuesView(collections.abc.Set):
672
673     __slots__ = ('_values',)
674
675     def __init__(self, values):
676         self._values = values
677
678     def __len__(self):
679         return len(self._values)
680
681     def __contains__(self, value):
682         return value in self._values
683
684     def __iter__(self):
685         yield from self._values
686
687
688 collections.abc.ValuesView.register(PropertiesValuesView)
689
690
691 class MessageIter(base.NotmuchIter):
692
693     def __init__(self, parent, msgs_p, *, db):
694         self._db = 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)
700
701     def __next__(self):
702         msg_p = super().__next__()
703         return Message(self, msg_p, db=self._db)