]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_message.py
bb5614260e8046a1ceb4e7b3450d0200ab684939
[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.
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         if not self._parent.alive:
65             return False
66         try:
67             self._msg_p
68         except errors.ObjectDestroyedError:
69             return False
70         else:
71             return True
72
73     def __del__(self):
74         self._destroy()
75
76     def _destroy(self):
77         if self.alive:
78             capi.lib.notmuch_message_destroy(self._msg_p)
79         self._msg_p = None
80
81     @property
82     def messageid(self):
83         """The message ID as a string.
84
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
89         :attr:`messageidb`.
90
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.
95
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
99            it.
100
101         :raises ObjectDestroyedError: if used after destoryed.
102         """
103         ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
104         return base.BinString(capi.ffi.string(ret))
105
106     @property
107     def threadid(self):
108         """The thread ID.
109
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.
113
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
117            it.
118
119         :raises ObjectDestroyedError: if used after destoryed.
120         """
121         ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
122         return base.BinString(capi.ffi.string(ret))
123
124     @property
125     def path(self):
126         """A pathname of the message as a pathlib.Path instance.
127
128         If multiple files in the database contain the same message ID
129         this will be just one of the files, chosen at random.
130
131         :raises ObjectDestroyedError: if used after destoryed.
132         """
133         ret = capi.lib.notmuch_message_get_filename(self._msg_p)
134         return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
135
136     @property
137     def pathb(self):
138         """A pathname of the message as a bytes object.
139
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.
142
143         :raises ObjectDestroyedError: if used after destoryed.
144         """
145         ret = capi.lib.notmuch_message_get_filename(self._msg_p)
146         return capi.ffi.string(ret)
147
148     def filenames(self):
149         """Return an iterator of all files for this message.
150
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`.
154
155         :returns: Iterator yielding :class:`pathlib.Path` instances.
156         :rtype: iter
157
158         :raises ObjectDestroyedError: if used after destoryed.
159         """
160         fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
161         return PathIter(self, fnames_p)
162
163     def filenamesb(self):
164         """Return an iterator of all files for this message.
165
166         This is like :meth:`pathnames` but the files are returned as
167         byte objects instead.
168
169         :returns: Iterator yielding :class:`bytes` instances.
170         :rtype: iter
171
172         :raises ObjectDestroyedError: if used after destoryed.
173         """
174         fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
175         return FilenamesIter(self, fnames_p)
176
177     @property
178     def ghost(self):
179         """Indicates whether this message is a ghost message.
180
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
185         for it.
186
187         :raises ObjectDestroyedError: if used after destoryed.
188         """
189         ret = capi.lib.notmuch_message_get_flag(
190             self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
191         return bool(ret)
192
193     @property
194     def excluded(self):
195         """Indicates whether this message was excluded from the query.
196
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
202         being set to *True*.
203
204         :raises ObjectDestroyedError: if used after destoryed.
205         """
206         ret = capi.lib.notmuch_message_get_flag(
207             self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
208         return bool(ret)
209
210     @property
211     def date(self):
212         """The message date as an integer.
213
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
217         :meth:`header`.
218
219         :raises ObjectDestroyedError: if used after destoryed.
220         """
221         return capi.lib.notmuch_message_get_date(self._msg_p)
222
223     def header(self, name):
224         """Return the value of the named header.
225
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.
233
234         :param header: Case-insensitive header name to retrieve.
235         :type header: str or bytes
236
237         :returns: The header value, an empty string if the header is
238            not present.
239         :rtype: str
240
241         :raises LookupError: if the header is not present.
242         :raises NullPointerError: For unexpected notmuch errors.
243         :raises ObjectDestroyedError: if used after destoryed.
244         """
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)
253         if not hdr:
254             raise LookupError
255         return hdr.decode(encoding='utf-8')
256
257     @property
258     def tags(self):
259         """The tags associated with the message.
260
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.
263
264         :raises ReadOnlyDatabaseError: When manipulating tags on a
265            database opened in read-only mode.
266         :raises ObjectDestroyedError: if used after destoryed.
267         """
268         try:
269             ref = self._cached_tagset
270         except AttributeError:
271             tagset = None
272         else:
273             tagset = ref()
274         if tagset is None:
275             tagset = tags.MutableTagSet(
276                 self, '_msg_p', capi.lib.notmuch_message_get_tags)
277             self._cached_tagset = weakref.ref(tagset)
278         return tagset
279
280     @contextlib.contextmanager
281     def frozen(self):
282         """Context manager to freeze the message state.
283
284         This allows you to perform atomic tag updates::
285
286            with msg.frozen():
287                msg.tags.clear()
288                msg.tags.add('foo')
289
290         Using This would ensure the message never ends up with no tags
291         applied at all.
292
293         It is safe to nest calls to this context manager.
294
295         :raises ReadOnlyDatabaseError: if the database is opened in
296            read-only mode.
297         :raises UnbalancedFreezeThawError: if you somehow managed to
298            call __exit__ of this context manager more than once.  Why
299            did you do that?
300         :raises ObjectDestroyedError: if used after destoryed.
301         """
302         ret = capi.lib.notmuch_message_freeze(self._msg_p)
303         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
304             raise errors.NotmuchError(ret)
305         self._frozen = True
306         try:
307             yield
308         except Exception:
309             # Only way to "rollback" these changes is to destroy
310             # ourselves and re-create.  Behold.
311             msgid = self.messageid
312             self._destroy()
313             with contextlib.suppress(Exception):
314                 new = self._db.find(msgid)
315                 self._msg_p = new._msg_p
316                 new._msg_p = None
317                 del new
318             raise
319         else:
320             ret = capi.lib.notmuch_message_thaw(self._msg_p)
321             if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
322                 raise errors.NotmuchError(ret)
323             self._frozen = False
324
325     @property
326     def properties(self):
327         """A map of arbitrary key-value pairs associated with the message.
328
329         Be aware that properties may be used by other extensions to
330         store state in.  So delete or modify with care.
331
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
338         value.
339
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.
344         """
345         try:
346             ref = self._cached_props
347         except AttributeError:
348             props = None
349         else:
350             props = ref()
351         if props is None:
352             props = PropertiesMap(self, '_msg_p')
353             self._cached_props = weakref.ref(props)
354         return props
355
356     def replies(self):
357         """Return an iterator of all replies to this message.
358
359         This method will only work if the message was created from a
360         thread.  Otherwise it will yield no results.
361
362         :returns: An iterator yielding :class:`Message` instances.
363         :rtype: MessageIter
364         """
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)
370
371     def __hash__(self):
372         return hash(self.messageid)
373
374     def __eq__(self, other):
375         if isinstance(other, self.__class__):
376             return self.messageid == other.messageid
377
378
379 class FilenamesIter(base.NotmuchIter):
380     """Iterator for binary filenames objects."""
381
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)
388
389     def __next__(self):
390         fname = super().__next__()
391         return capi.ffi.string(fname)
392
393
394 class PathIter(FilenamesIter):
395     """Iterator for pathlib.Path objects."""
396
397     def __next__(self):
398         fname = super().__next__()
399         return pathlib.Path(os.fsdecode(fname))
400
401
402 class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
403     """A mutable mapping to manage properties.
404
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`.
409
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.
417
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
420     O(1).
421     """
422     Property = collections.namedtuple('Property', ['key', 'value'])
423     _marker = object()
424
425     def __init__(self, msg, ptr_name):
426         self._msg = msg
427         self._ptr = lambda: getattr(msg, ptr_name)
428
429     @property
430     def alive(self):
431         if not self._msg.alive:
432             return False
433         try:
434             self._ptr
435         except errors.ObjectDestroyedError:
436             return False
437         else:
438             return True
439
440     def _destroy(self):
441         pass
442
443     def __iter__(self):
444         """Return an iterator which iterates over the keys.
445
446         Be aware that a single key may have multiple values associated
447         with it, if so it will appear multiple times here.
448         """
449         iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
450         return PropertiesKeyIter(self, iter_p)
451
452     def __len__(self):
453         iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
454         it = base.NotmuchIter(
455             self, iter_p,
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,
460         )
461         return len(list(it))
462
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:
472             raise KeyError
473         return base.BinString.from_cffi(value_pp[0])
474
475     def keys(self):
476         """Return a :class:`collections.abc.KeysView` for this map.
477
478         Even when keys occur multiple times this is a subset of set()
479         so will only contain them once.
480         """
481         return collections.abc.KeysView({k: None for k in self})
482
483     def items(self):
484         """Return a :class:`collections.abc.ItemsView` for this map.
485
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.
489         """
490         items = set()
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)
500
501     def values(self):
502         """Return a :class:`collecions.abc.ValuesView` for this map.
503
504         All unique property values are included in the view.
505         """
506         values = set()
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)
514
515     def __setitem__(self, key, value):
516         """Add a key-value pair to the properties.
517
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.
521         """
522         self.add(key, value)
523
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)
533
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)
541
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)
551
552     def pop(self, key, default=_marker):
553         try:
554             value = self[key]
555         except KeyError:
556             if default is self._marker:
557                 raise
558             else:
559                 return default
560         else:
561             self.remove(key, value)
562             return value
563
564     def popitem(self):
565         try:
566             key = next(iter(self))
567         except StopIteration:
568             raise KeyError
569         value = self.pop(key)
570         return (key, value)
571
572     def clear(self):
573         ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
574                                                              capi.ffi.NULL)
575         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
576             raise errors.NotmuchError(ret)
577
578     def getall(self, prefix='', *, exact=False):
579         """Return an iterator yielding all properties for a given key prefix.
580
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.
586
587         :param prefix: The prefix of the key.
588         """
589         if isinstance(prefix, str):
590             prefix = prefix.encode('utf-8')
591         props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
592                                                           prefix, exact)
593         return PropertiesIter(self, props_p)
594
595
596 class PropertiesKeyIter(base.NotmuchIter):
597
598     def __init__(self, parent, iter_p):
599         super().__init__(
600             parent,
601             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)
606
607     def __next__(self):
608         item = super().__next__()
609         return base.BinString.from_cffi(item)
610
611
612 class PropertiesIter(base.NotmuchIter):
613
614     def __init__(self, parent, iter_p):
615         super().__init__(
616             parent,
617             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,
622         )
623
624     def __next__(self):
625         if not self._fn_valid(self._iter_p):
626             self._destroy()
627             raise StopIteration
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))
633
634
635 class PropertiesItemsView(collections.abc.Set):
636
637     __slots__ = ('_items',)
638
639     def __init__(self, items):
640         self._items = items
641
642     @classmethod
643     def _from_iterable(self, it):
644         return set(it)
645
646     def __len__(self):
647         return len(self._items)
648
649     def __contains__(self, item):
650         return item in self._items
651
652     def __iter__(self):
653         yield from self._items
654
655
656 collections.abc.ItemsView.register(PropertiesItemsView)
657
658
659 class PropertiesValuesView(collections.abc.Set):
660
661     __slots__ = ('_values',)
662
663     def __init__(self, values):
664         self._values = values
665
666     def __len__(self):
667         return len(self._values)
668
669     def __contains__(self, value):
670         return value in self._values
671
672     def __iter__(self):
673         yield from self._values
674
675
676 collections.abc.ValuesView.register(PropertiesValuesView)
677
678
679 class MessageIter(base.NotmuchIter):
680
681     def __init__(self, parent, msgs_p, *, db):
682         self._db = 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)
688
689     def __next__(self):
690         msg_p = super().__next__()
691         return Message(self, msg_p, db=self._db)