]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_message.py
Merge branch 'release'
[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     :param dup: Whether the message was a duplicate on insertion.
51     :type dup: None or bool
52     """
53     _msg_p = base.MemoryPointer()
54
55     def __init__(self, parent, msg_p, *, db):
56         self._parent = parent
57         self._msg_p = msg_p
58         self._db = db
59
60     @property
61     def alive(self):
62         if not self._parent.alive:
63             return False
64         try:
65             self._msg_p
66         except errors.ObjectDestroyedError:
67             return False
68         else:
69             return True
70
71     def __del__(self):
72         self._destroy()
73
74     def _destroy(self):
75         if self.alive:
76             capi.lib.notmuch_message_destroy(self._msg_p)
77         self._msg_p = None
78
79     @property
80     def messageid(self):
81         """The message ID as a string.
82
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
87         :attr:`messageidb`.
88
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.
93
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
97            it.
98
99         :raises ObjectDestroyedError: if used after destroyed.
100         """
101         ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
102         return base.BinString(capi.ffi.string(ret))
103
104     @property
105     def threadid(self):
106         """The thread ID.
107
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.
111
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
115            it.
116
117         :raises ObjectDestroyedError: if used after destroyed.
118         """
119         ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
120         return base.BinString(capi.ffi.string(ret))
121
122     @property
123     def path(self):
124         """A pathname of the message as a pathlib.Path instance.
125
126         If multiple files in the database contain the same message ID
127         this will be just one of the files, chosen at random.
128
129         :raises ObjectDestroyedError: if used after destroyed.
130         """
131         ret = capi.lib.notmuch_message_get_filename(self._msg_p)
132         return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
133
134     @property
135     def pathb(self):
136         """A pathname of the message as a bytes object.
137
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.
140
141         :raises ObjectDestroyedError: if used after destroyed.
142         """
143         ret = capi.lib.notmuch_message_get_filename(self._msg_p)
144         return capi.ffi.string(ret)
145
146     def filenames(self):
147         """Return an iterator of all files for this message.
148
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`.
152
153         :returns: Iterator yielding :class:`pathlib.Path` instances.
154         :rtype: iter
155
156         :raises ObjectDestroyedError: if used after destroyed.
157         """
158         fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
159         return PathIter(self, fnames_p)
160
161     def filenamesb(self):
162         """Return an iterator of all files for this message.
163
164         This is like :meth:`pathnames` but the files are returned as
165         byte objects instead.
166
167         :returns: Iterator yielding :class:`bytes` instances.
168         :rtype: iter
169
170         :raises ObjectDestroyedError: if used after destroyed.
171         """
172         fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
173         return FilenamesIter(self, fnames_p)
174
175     @property
176     def ghost(self):
177         """Indicates whether this message is a ghost message.
178
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
183         for it.
184
185         :raises ObjectDestroyedError: if used after destroyed.
186         """
187         ret = capi.lib.notmuch_message_get_flag(
188             self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
189         return bool(ret)
190
191     @property
192     def excluded(self):
193         """Indicates whether this message was excluded from the query.
194
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
200         being set to *True*.
201
202         :raises ObjectDestroyedError: if used after destroyed.
203         """
204         ret = capi.lib.notmuch_message_get_flag(
205             self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
206         return bool(ret)
207
208     @property
209     def matched(self):
210         """Indicates whether this message was matched by the query.
211
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.
215
216         :raises ObjectDestroyedError: if used after destroyed.
217         """
218         ret = capi.lib.notmuch_message_get_flag(
219             self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_MATCH)
220         return bool(ret)
221
222     @property
223     def date(self):
224         """The message date as an integer.
225
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
229         :meth:`header`.
230
231         :raises ObjectDestroyedError: if used after destroyed.
232         """
233         return capi.lib.notmuch_message_get_date(self._msg_p)
234
235     def header(self, name):
236         """Return the value of the named header.
237
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.
245
246         :param header: Case-insensitive header name to retrieve.
247         :type header: str or bytes
248
249         :returns: The header value, an empty string if the header is
250            not present.
251         :rtype: str
252
253         :raises LookupError: if the header is not present.
254         :raises NullPointerError: For unexpected notmuch errors.
255         :raises ObjectDestroyedError: if used after destroyed.
256         """
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)
265         if not hdr:
266             raise LookupError
267         return hdr.decode(encoding='utf-8')
268
269     @property
270     def tags(self):
271         """The tags associated with the message.
272
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.
275
276         :raises ReadOnlyDatabaseError: When manipulating tags on a
277            database opened in read-only mode.
278         :raises ObjectDestroyedError: if used after destroyed.
279         """
280         try:
281             ref = self._cached_tagset
282         except AttributeError:
283             tagset = None
284         else:
285             tagset = ref()
286         if tagset is None:
287             tagset = tags.MutableTagSet(
288                 self, '_msg_p', capi.lib.notmuch_message_get_tags)
289             self._cached_tagset = weakref.ref(tagset)
290         return tagset
291
292     @contextlib.contextmanager
293     def frozen(self):
294         """Context manager to freeze the message state.
295
296         This allows you to perform atomic tag updates::
297
298            with msg.frozen():
299                msg.tags.clear()
300                msg.tags.add('foo')
301
302         Using This would ensure the message never ends up with no tags
303         applied at all.
304
305         It is safe to nest calls to this context manager.
306
307         :raises ReadOnlyDatabaseError: if the database is opened in
308            read-only mode.
309         :raises UnbalancedFreezeThawError: if you somehow managed to
310            call __exit__ of this context manager more than once.  Why
311            did you do that?
312         :raises ObjectDestroyedError: if used after destroyed.
313         """
314         ret = capi.lib.notmuch_message_freeze(self._msg_p)
315         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
316             raise errors.NotmuchError(ret)
317         self._frozen = True
318         try:
319             yield
320         except Exception:
321             # Only way to "rollback" these changes is to destroy
322             # ourselves and re-create.  Behold.
323             msgid = self.messageid
324             self._destroy()
325             with contextlib.suppress(Exception):
326                 new = self._db.find(msgid)
327                 self._msg_p = new._msg_p
328                 new._msg_p = None
329                 del new
330             raise
331         else:
332             ret = capi.lib.notmuch_message_thaw(self._msg_p)
333             if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
334                 raise errors.NotmuchError(ret)
335             self._frozen = False
336
337     @property
338     def properties(self):
339         """A map of arbitrary key-value pairs associated with the message.
340
341         Be aware that properties may be used by other extensions to
342         store state in.  So delete or modify with care.
343
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
350         value.
351
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.
356         """
357         try:
358             ref = self._cached_props
359         except AttributeError:
360             props = None
361         else:
362             props = ref()
363         if props is None:
364             props = PropertiesMap(self, '_msg_p')
365             self._cached_props = weakref.ref(props)
366         return props
367
368     def replies(self):
369         """Return an iterator of all replies to this message.
370
371         This method will only work if the message was created from a
372         thread.  Otherwise it will yield no results.
373
374         :returns: An iterator yielding :class:`OwnedMessage` instances.
375         :rtype: MessageIter
376         """
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)
382
383     def __hash__(self):
384         return hash(self.messageid)
385
386     def __eq__(self, other):
387         if isinstance(other, self.__class__):
388             return self.messageid == other.messageid
389
390
391 class OwnedMessage(Message):
392     """An email message owned by parent thread object.
393
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.
401     """
402
403     @property
404     def alive(self):
405         return self._parent.alive
406
407     def _destroy(self):
408         pass
409
410
411 class FilenamesIter(base.NotmuchIter):
412     """Iterator for binary filenames objects."""
413
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)
420
421     def __next__(self):
422         fname = super().__next__()
423         return capi.ffi.string(fname)
424
425
426 class PathIter(FilenamesIter):
427     """Iterator for pathlib.Path objects."""
428
429     def __next__(self):
430         fname = super().__next__()
431         return pathlib.Path(os.fsdecode(fname))
432
433
434 class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
435     """A mutable mapping to manage properties.
436
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`.
441
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.
449
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
452     O(1).
453     """
454     Property = collections.namedtuple('Property', ['key', 'value'])
455     _marker = object()
456
457     def __init__(self, msg, ptr_name):
458         self._msg = msg
459         self._ptr = lambda: getattr(msg, ptr_name)
460
461     @property
462     def alive(self):
463         if not self._msg.alive:
464             return False
465         try:
466             self._ptr
467         except errors.ObjectDestroyedError:
468             return False
469         else:
470             return True
471
472     def _destroy(self):
473         pass
474
475     def __iter__(self):
476         """Return an iterator which iterates over the keys.
477
478         Be aware that a single key may have multiple values associated
479         with it, if so it will appear multiple times here.
480         """
481         iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
482         return PropertiesKeyIter(self, iter_p)
483
484     def __len__(self):
485         iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
486         it = base.NotmuchIter(
487             self, iter_p,
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,
492         )
493         return len(list(it))
494
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:
504             raise KeyError
505         return base.BinString.from_cffi(value_pp[0])
506
507     def keys(self):
508         """Return a :class:`collections.abc.KeysView` for this map.
509
510         Even when keys occur multiple times this is a subset of set()
511         so will only contain them once.
512         """
513         return collections.abc.KeysView({k: None for k in self})
514
515     def items(self):
516         """Return a :class:`collections.abc.ItemsView` for this map.
517
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.
521         """
522         items = set()
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)
532
533     def values(self):
534         """Return a :class:`collecions.abc.ValuesView` for this map.
535
536         All unique property values are included in the view.
537         """
538         values = set()
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)
546
547     def __setitem__(self, key, value):
548         """Add a key-value pair to the properties.
549
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.
553         """
554         self.add(key, value)
555
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)
565
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)
573
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)
583
584     def pop(self, key, default=_marker):
585         try:
586             value = self[key]
587         except KeyError:
588             if default is self._marker:
589                 raise
590             else:
591                 return default
592         else:
593             self.remove(key, value)
594             return value
595
596     def popitem(self):
597         try:
598             key = next(iter(self))
599         except StopIteration:
600             raise KeyError
601         value = self.pop(key)
602         return (key, value)
603
604     def clear(self):
605         ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
606                                                              capi.ffi.NULL)
607         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
608             raise errors.NotmuchError(ret)
609
610     def getall(self, prefix='', *, exact=False):
611         """Return an iterator yielding all properties for a given key prefix.
612
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.
618
619         :param prefix: The prefix of the key.
620         """
621         if isinstance(prefix, str):
622             prefix = prefix.encode('utf-8')
623         props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
624                                                           prefix, exact)
625         return PropertiesIter(self, props_p)
626
627
628 class PropertiesKeyIter(base.NotmuchIter):
629
630     def __init__(self, parent, iter_p):
631         super().__init__(
632             parent,
633             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)
638
639     def __next__(self):
640         item = super().__next__()
641         return base.BinString.from_cffi(item)
642
643
644 class PropertiesIter(base.NotmuchIter):
645
646     def __init__(self, parent, iter_p):
647         super().__init__(
648             parent,
649             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,
654         )
655
656     def __next__(self):
657         if not self._fn_valid(self._iter_p):
658             self._destroy()
659             raise StopIteration
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))
665
666
667 class PropertiesItemsView(collections.abc.Set):
668
669     __slots__ = ('_items',)
670
671     def __init__(self, items):
672         self._items = items
673
674     @classmethod
675     def _from_iterable(self, it):
676         return set(it)
677
678     def __len__(self):
679         return len(self._items)
680
681     def __contains__(self, item):
682         return item in self._items
683
684     def __iter__(self):
685         yield from self._items
686
687
688 collections.abc.ItemsView.register(PropertiesItemsView)
689
690
691 class PropertiesValuesView(collections.abc.Set):
692
693     __slots__ = ('_values',)
694
695     def __init__(self, values):
696         self._values = values
697
698     def __len__(self):
699         return len(self._values)
700
701     def __contains__(self, value):
702         return value in self._values
703
704     def __iter__(self):
705         yield from self._values
706
707
708 collections.abc.ValuesView.register(PropertiesValuesView)
709
710
711 class MessageIter(base.NotmuchIter):
712
713     def __init__(self, parent, msgs_p, *, db, msg_cls=Message):
714         self._db = db
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)
721
722     def __next__(self):
723         msg_p = super().__next__()
724         return self._msg_cls(self, msg_p, db=self._db)