1import collections
2import configparser
3import enum
4import functools
5import os
6import pathlib
7import weakref
8
9import notmuch2._base as base
10import notmuch2._config as config
11import notmuch2._capi as capi
12import notmuch2._errors as errors
13import notmuch2._message as message
14import notmuch2._query as querymod
15import notmuch2._tags as tags
16
17
18__all__ = ['Database', 'AtomicContext', 'DbRevision']
19
20
21def _config_pathname():
22    """Return the path of the configuration file.
23
24    :rtype: pathlib.Path
25    """
26    cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
27    return pathlib.Path(os.path.expanduser(cfgfname))
28
29
30class Mode(enum.Enum):
31    READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
32    READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
33
34class ConfigFile(enum.Enum):
35    EMPTY = b''
36    SEARCH = capi.ffi.NULL
37
38class QuerySortOrder(enum.Enum):
39    OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
40    NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
41    MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
42    UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
43
44
45class QueryExclude(enum.Enum):
46    TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
47    FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
48    FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
49    ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
50
51
52class DecryptionPolicy(enum.Enum):
53    FALSE = capi.lib.NOTMUCH_DECRYPT_FALSE
54    TRUE = capi.lib.NOTMUCH_DECRYPT_TRUE
55    AUTO = capi.lib.NOTMUCH_DECRYPT_AUTO
56    NOSTASH = capi.lib.NOTMUCH_DECRYPT_NOSTASH
57
58
59class Database(base.NotmuchObject):
60    """Toplevel access to notmuch.
61
62    A :class:`Database` can be opened read-only or read-write.
63    Modifications are not atomic by default, use :meth:`begin_atomic`
64    for atomic updates.  If the underlying database has been modified
65    outside of this class a :exc:`XapianError` will be raised and the
66    instance must be closed and a new one created.
67
68    You can use an instance of this class as a context-manager.
69
70    :cvar MODE: The mode a database can be opened with, an enumeration
71       of ``READ_ONLY`` and ``READ_WRITE``
72    :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
73       ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
74    :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
75       ``FLAG``, ``FALSE`` or ``ALL``.  See the query documentation
76       for details.
77    :cvar CONFIG: Control loading of config file. Enumeration of
78       ``EMPTY`` (don't load a config file), and ``SEARCH`` (search as
79       in :ref:`config_search`)
80    :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
81       :meth:`add` as return value.
82    :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
83       This is used to implement the ``ro`` and ``rw`` string
84       variants.
85
86    :ivar closed: Boolean indicating if the database is closed or
87       still open.
88
89    :param path: The directory of where the database is stored.  If
90       ``None`` the location will be searched according to
91       :ref:`database`
92    :type path: str, bytes, os.PathLike or pathlib.Path
93    :param mode: The mode to open the database in.  One of
94       :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`.  For
95       convenience you can also use the strings ``ro`` for
96       :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
97    :type mode: :attr:`MODE` or str.
98
99    :param config: Where to load the configuration from, if any.
100    :type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path
101    :raises KeyError: if an unknown mode string is used.
102    :raises OSError: or subclasses if the configuration file can not
103       be opened.
104    :raises configparser.Error: or subclasses if the configuration
105       file can not be parsed.
106    :raises NotmuchError: or subclasses for other failures.
107    """
108
109    MODE = Mode
110    SORT = QuerySortOrder
111    EXCLUDE = QueryExclude
112    CONFIG = ConfigFile
113    AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
114    _db_p = base.MemoryPointer()
115    STR_MODE_MAP = {
116        'ro': MODE.READ_ONLY,
117        'rw': MODE.READ_WRITE,
118    }
119
120    @staticmethod
121    def _cfg_path_encode(path):
122        if isinstance(path,ConfigFile):
123            path = path.value
124        elif path is None:
125            path = capi.ffi.NULL
126        elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
127            path = bytes(path)
128        else:
129            path = os.fsencode(path)
130        return path
131
132    @staticmethod
133    def _db_path_encode(path):
134        if path is None:
135            path = capi.ffi.NULL
136        elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
137            path = bytes(path)
138        else:
139            path = os.fsencode(path)
140        return path
141
142    def __init__(self, path=None, mode=MODE.READ_ONLY, config=CONFIG.EMPTY):
143        if isinstance(mode, str):
144            mode = self.STR_MODE_MAP[mode]
145        self.mode = mode
146
147        db_pp = capi.ffi.new('notmuch_database_t **')
148        cmsg = capi.ffi.new('char**')
149        ret = capi.lib.notmuch_database_open_with_config(self._db_path_encode(path),
150                                                         mode.value,
151                                                         self._cfg_path_encode(config),
152                                                         capi.ffi.NULL,
153                                                         db_pp, cmsg)
154        if cmsg[0]:
155            msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
156            capi.lib.free(cmsg[0])
157        else:
158            msg = None
159        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
160            raise errors.NotmuchError(ret, msg)
161        self._db_p = db_pp[0]
162        self.closed = False
163
164    @classmethod
165    def create(cls, path=None, config=ConfigFile.EMPTY):
166        """Create and open database in READ_WRITE mode.
167
168        This is creates a new notmuch database and returns an opened
169        instance in :attr:`MODE.READ_WRITE` mode.
170
171        :param path: The directory of where the database is stored.
172           If ``None`` the location will be read searched by the
173           notmuch library (see notmuch(3)::notmuch_open_with_config).
174        :type path: str, bytes or os.PathLike
175
176        :param config: The pathname of the notmuch configuration file.
177        :type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path
178
179        :raises OSError: or subclasses if the configuration file can not
180           be opened.
181        :raises configparser.Error: or subclasses if the configuration
182           file can not be parsed.
183        :raises NotmuchError: if the config file does not have the
184           database.path setting.
185        :raises FileError: if the database already exists.
186
187        :returns: The newly created instance.
188        """
189
190        db_pp = capi.ffi.new('notmuch_database_t **')
191        cmsg = capi.ffi.new('char**')
192        ret = capi.lib.notmuch_database_create_with_config(cls._db_path_encode(path),
193                                                           cls._cfg_path_encode(config),
194                                                           capi.ffi.NULL,
195                                                           db_pp, cmsg)
196        if cmsg[0]:
197            msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
198            capi.lib.free(cmsg[0])
199        else:
200            msg = None
201        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
202            raise errors.NotmuchError(ret, msg)
203
204        # Now close the db and let __init__ open it.  Inefficient but
205        # creating is not a hot loop while this allows us to have a
206        # clean API.
207        ret = capi.lib.notmuch_database_destroy(db_pp[0])
208        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
209            raise errors.NotmuchError(ret)
210        return cls(path, cls.MODE.READ_WRITE, config=config)
211
212    @staticmethod
213    def default_path(cfg_path=None):
214        """Return the path of the user's default database.
215
216        This reads the user's configuration file and returns the
217        default path of the database.
218
219        :param cfg_path: The pathname of the notmuch configuration file.
220           If not specified tries to use the pathname provided in the
221           :envvar:`NOTMUCH_CONFIG` environment variable and falls back
222           to :file:`~/.notmuch-config`.
223        :type cfg_path: str, bytes, os.PathLike or pathlib.Path.
224
225        :returns: The path of the database, which does not necessarily
226           exists.
227        :rtype: pathlib.Path
228        :raises OSError: or subclasses if the configuration file can not
229           be opened.
230        :raises configparser.Error: or subclasses if the configuration
231           file can not be parsed.
232        :raises NotmuchError: if the config file does not have the
233           database.path setting.
234
235        .. deprecated:: 0.35
236           Use the ``config`` parameter to :meth:`__init__` or :meth:`__create__` instead.
237        """
238        if not cfg_path:
239            cfg_path = _config_pathname()
240        if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
241            cfg_path = bytes(cfg_path)
242        parser = configparser.ConfigParser()
243        with open(cfg_path) as fp:
244            parser.read_file(fp)
245        try:
246            return pathlib.Path(parser.get('database', 'path'))
247        except configparser.Error:
248            raise errors.NotmuchError(
249                'No database.path setting in {}'.format(cfg_path))
250
251    def __del__(self):
252        self._destroy()
253
254    @property
255    def alive(self):
256        try:
257            self._db_p
258        except errors.ObjectDestroyedError:
259            return False
260        else:
261            return True
262
263    def _destroy(self):
264        try:
265            ret = capi.lib.notmuch_database_destroy(self._db_p)
266        except errors.ObjectDestroyedError:
267            ret = capi.lib.NOTMUCH_STATUS_SUCCESS
268        else:
269            self._db_p = None
270        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
271            raise errors.NotmuchError(ret)
272
273    def close(self):
274        """Close the notmuch database.
275
276        Once closed most operations will fail.  This can still be
277        useful however to explicitly close a database which is opened
278        read-write as this would otherwise stop other processes from
279        reading the database while it is open.
280
281        :raises ObjectDestroyedError: if used after destroyed.
282        """
283        ret = capi.lib.notmuch_database_close(self._db_p)
284        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
285            raise errors.NotmuchError(ret)
286        self.closed = True
287
288    def __enter__(self):
289        return self
290
291    def __exit__(self, exc_type, exc_value, traceback):
292        self.close()
293
294    @property
295    def path(self):
296        """The pathname of the notmuch database.
297
298        This is returned as a :class:`pathlib.Path` instance.
299
300        :raises ObjectDestroyedError: if used after destroyed.
301        """
302        try:
303            return self._cache_path
304        except AttributeError:
305            ret = capi.lib.notmuch_database_get_path(self._db_p)
306            self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
307            return self._cache_path
308
309    @property
310    def version(self):
311        """The database format version.
312
313        This is a positive integer.
314
315        :raises ObjectDestroyedError: if used after destroyed.
316        """
317        try:
318            return self._cache_version
319        except AttributeError:
320            ret = capi.lib.notmuch_database_get_version(self._db_p)
321            self._cache_version = ret
322            return ret
323
324    @property
325    def needs_upgrade(self):
326        """Whether the database should be upgraded.
327
328        If *True* the database can be upgraded using :meth:`upgrade`.
329        Not doing so may result in some operations raising
330        :exc:`UpgradeRequiredError`.
331
332        A read-only database will never be upgradable.
333
334        :raises ObjectDestroyedError: if used after destroyed.
335        """
336        ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
337        return bool(ret)
338
339    def upgrade(self, progress_cb=None):
340        """Upgrade the database to the latest version.
341
342        Upgrade the database, optionally with a progress callback
343        which should be a callable which will be called with a
344        floating point number in the range of [0.0 .. 1.0].
345        """
346        raise NotImplementedError
347
348    def atomic(self):
349        """Return a context manager to perform atomic operations.
350
351        The returned context manager can be used to perform atomic
352        operations on the database.
353
354        .. note:: Unlinke a traditional RDBMS transaction this does
355           not imply durability, it only ensures the changes are
356           performed atomically.
357
358        :raises ObjectDestroyedError: if used after destroyed.
359        """
360        ctx = AtomicContext(self, '_db_p')
361        return ctx
362
363    def revision(self):
364        """The currently committed revision in the database.
365
366        Returned as a ``(revision, uuid)`` namedtuple.
367
368        :raises ObjectDestroyedError: if used after destroyed.
369        """
370        raw_uuid = capi.ffi.new('char**')
371        rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
372        return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
373
374    def get_directory(self, path):
375        raise NotImplementedError
376
377    def default_indexopts(self):
378        """Returns default index options for the database.
379
380        :raises ObjectDestroyedError: if used after destroyed.
381
382        :returns: :class:`IndexOptions`.
383        """
384        opts = capi.lib.notmuch_database_get_default_indexopts(self._db_p)
385        return IndexOptions(self, opts)
386
387    def add(self, filename, *, sync_flags=False, indexopts=None):
388        """Add a message to the database.
389
390        Add a new message to the notmuch database.  The message is
391        referred to by the pathname of the maildir file.  If the
392        message ID of the new message already exists in the database,
393        this adds ``pathname`` to the list of list of files for the
394        existing message.
395
396        :param filename: The path of the file containing the message.
397        :type filename: str, bytes, os.PathLike or pathlib.Path.
398        :param sync_flags: Whether to sync the known maildir flags to
399           notmuch tags.  See :meth:`Message.flags_to_tags` for
400           details.
401        :type sync_flags: bool
402        :param indexopts: The indexing options, see
403           :meth:`default_indexopts`.  Leave as `None` to use the
404           default options configured in the database.
405        :type indexopts: :class:`IndexOptions` or `None`
406
407        :returns: A tuple where the first item is the newly inserted
408           messages as a :class:`Message` instance, and the second
409           item is a boolean indicating if the message inserted was a
410           duplicate.  This is the namedtuple ``AddedMessage(msg,
411           dup)``.
412        :rtype: Database.AddedMessage
413
414        If an exception is raised, no message was added.
415
416        :raises XapianError: A Xapian exception occurred.
417        :raises FileError: The file referred to by ``pathname`` could
418           not be opened.
419        :raises FileNotEmailError: The file referreed to by
420           ``pathname`` is not recognised as an email message.
421        :raises ReadOnlyDatabaseError: The database is opened in
422           READ_ONLY mode.
423        :raises UpgradeRequiredError: The database must be upgraded
424           first.
425        :raises ObjectDestroyedError: if used after destroyed.
426        """
427        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
428            filename = bytes(filename)
429        msg_pp = capi.ffi.new('notmuch_message_t **')
430        opts_p = indexopts._opts_p if indexopts else capi.ffi.NULL
431        ret = capi.lib.notmuch_database_index_file(
432            self._db_p, os.fsencode(filename), opts_p, msg_pp)
433        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
434              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
435        if ret not in ok:
436            raise errors.NotmuchError(ret)
437        msg = message.Message(self, msg_pp[0], db=self)
438        if sync_flags:
439            msg.tags.from_maildir_flags()
440        return self.AddedMessage(
441            msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
442
443    def remove(self, filename):
444        """Remove a message from the notmuch database.
445
446        Removing a message which is not in the database is just a
447        silent nop-operation.
448
449        :param filename: The pathname of the file containing the
450           message to be removed.
451        :type filename: str, bytes, os.PathLike or pathlib.Path.
452
453        :returns: True if the message is still in the database.  This
454           can happen when multiple files contain the same message ID.
455           The true/false distinction is fairly arbitrary, but think
456           of it as ``dup = db.remove_message(name); if dup: ...``.
457        :rtype: bool
458
459        :raises XapianError: A Xapian exception occurred.
460        :raises ReadOnlyDatabaseError: The database is opened in
461           READ_ONLY mode.
462        :raises UpgradeRequiredError: The database must be upgraded
463           first.
464        :raises ObjectDestroyedError: if used after destroyed.
465        """
466        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
467            filename = bytes(filename)
468        ret = capi.lib.notmuch_database_remove_message(self._db_p,
469                                                       os.fsencode(filename))
470        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
471              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
472        if ret not in ok:
473            raise errors.NotmuchError(ret)
474        if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
475            return True
476        else:
477            return False
478
479    def find(self, msgid):
480        """Return the message matching the given message ID.
481
482        If a message with the given message ID is found a
483        :class:`Message` instance is returned.  Otherwise a
484        :exc:`LookupError` is raised.
485
486        :param msgid: The message ID to look for.
487        :type msgid: str
488
489        :returns: The message instance.
490        :rtype: Message
491
492        :raises LookupError: If no message was found.
493        :raises OutOfMemoryError: When there is no memory to allocate
494           the message instance.
495        :raises XapianError: A Xapian exception occurred.
496        :raises ObjectDestroyedError: if used after destroyed.
497        """
498        msg_pp = capi.ffi.new('notmuch_message_t **')
499        ret = capi.lib.notmuch_database_find_message(self._db_p,
500                                                     msgid.encode(), msg_pp)
501        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
502            raise errors.NotmuchError(ret)
503        msg_p = msg_pp[0]
504        if msg_p == capi.ffi.NULL:
505            raise LookupError
506        msg = message.Message(self, msg_p, db=self)
507        return msg
508
509    def get(self, filename):
510        """Return the :class:`Message` given a pathname.
511
512        If a message with the given pathname exists in the database
513        return the :class:`Message` instance for the message.
514        Otherwise raise a :exc:`LookupError` exception.
515
516        :param filename: The pathname of the message.
517        :type filename: str, bytes, os.PathLike or pathlib.Path
518
519        :returns: The message instance.
520        :rtype: Message
521
522        :raises LookupError: If no message was found.  This is also
523           a subclass of :exc:`KeyError`.
524        :raises OutOfMemoryError: When there is no memory to allocate
525           the message instance.
526        :raises XapianError: A Xapian exception occurred.
527        :raises ObjectDestroyedError: if used after destroyed.
528        """
529        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
530            filename = bytes(filename)
531        msg_pp = capi.ffi.new('notmuch_message_t **')
532        ret = capi.lib.notmuch_database_find_message_by_filename(
533            self._db_p, os.fsencode(filename), msg_pp)
534        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
535            raise errors.NotmuchError(ret)
536        msg_p = msg_pp[0]
537        if msg_p == capi.ffi.NULL:
538            raise LookupError
539        msg = message.Message(self, msg_p, db=self)
540        return msg
541
542    @property
543    def tags(self):
544        """Return an immutable set with all tags used in this database.
545
546        This returns an immutable set-like object implementing the
547        collections.abc.Set Abstract Base Class.  Due to the
548        underlying libnotmuch implementation some operations have
549        different performance characteristics then plain set objects.
550        Mainly any lookup operation is O(n) rather then O(1).
551
552        Normal usage treats tags as UTF-8 encoded unicode strings so
553        they are exposed to Python as normal unicode string objects.
554        If you need to handle tags stored in libnotmuch which are not
555        valid unicode do check the :class:`ImmutableTagSet` docs for
556        how to handle this.
557
558        :rtype: ImmutableTagSet
559
560        :raises ObjectDestroyedError: if used after destroyed.
561        """
562        try:
563            ref = self._cached_tagset
564        except AttributeError:
565            tagset = None
566        else:
567            tagset = ref()
568        if tagset is None:
569            tagset = tags.ImmutableTagSet(
570                self, '_db_p', capi.lib.notmuch_database_get_all_tags)
571            self._cached_tagset = weakref.ref(tagset)
572        return tagset
573
574    @property
575    def config(self):
576        """Return a mutable mapping with the settings stored in this database.
577
578        This returns an mutable dict-like object implementing the
579        collections.abc.MutableMapping Abstract Base Class.
580
581        :rtype: Config
582
583        :raises ObjectDestroyedError: if used after destroyed.
584        """
585        try:
586            ref = self._cached_config
587        except AttributeError:
588            config_mapping = None
589        else:
590            config_mapping = ref()
591        if config_mapping is None:
592            config_mapping = config.ConfigMapping(self, '_db_p')
593            self._cached_config = weakref.ref(config_mapping)
594        return config_mapping
595
596    def _create_query(self, query, *,
597                      omit_excluded=EXCLUDE.TRUE,
598                      sort=SORT.UNSORTED,  # Check this default
599                      exclude_tags=None):
600        """Create an internal query object.
601
602        :raises OutOfMemoryError: if no memory is available to
603           allocate the query.
604        """
605        if isinstance(query, str):
606            query = query.encode('utf-8')
607        query_p = capi.lib.notmuch_query_create(self._db_p, query)
608        if query_p == capi.ffi.NULL:
609            raise errors.OutOfMemoryError()
610        capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
611        capi.lib.notmuch_query_set_sort(query_p, sort.value)
612        if exclude_tags is not None:
613            for tag in exclude_tags:
614                if isinstance(tag, str):
615                    tag = tag.encode('utf-8')
616                capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
617        return querymod.Query(self, query_p)
618
619    def messages(self, query, *,
620                 omit_excluded=EXCLUDE.TRUE,
621                 sort=SORT.UNSORTED,  # Check this default
622                 exclude_tags=None):
623        """Search the database for messages.
624
625        :returns: An iterator over the messages found.
626        :rtype: MessageIter
627
628        :raises OutOfMemoryError: if no memory is available to
629           allocate the query.
630        :raises ObjectDestroyedError: if used after destroyed.
631        """
632        query = self._create_query(query,
633                                   omit_excluded=omit_excluded,
634                                   sort=sort,
635                                   exclude_tags=exclude_tags)
636        return query.messages()
637
638    def count_messages(self, query, *,
639                       omit_excluded=EXCLUDE.TRUE,
640                       sort=SORT.UNSORTED,  # Check this default
641                       exclude_tags=None):
642        """Search the database for messages.
643
644        :returns: An iterator over the messages found.
645        :rtype: MessageIter
646
647        :raises ObjectDestroyedError: if used after destroyed.
648        """
649        query = self._create_query(query,
650                                   omit_excluded=omit_excluded,
651                                   sort=sort,
652                                   exclude_tags=exclude_tags)
653        return query.count_messages()
654
655    def threads(self,  query, *,
656                omit_excluded=EXCLUDE.TRUE,
657                sort=SORT.UNSORTED,  # Check this default
658                exclude_tags=None):
659        query = self._create_query(query,
660                                   omit_excluded=omit_excluded,
661                                   sort=sort,
662                                   exclude_tags=exclude_tags)
663        return query.threads()
664
665    def count_threads(self, query, *,
666                      omit_excluded=EXCLUDE.TRUE,
667                      sort=SORT.UNSORTED,  # Check this default
668                      exclude_tags=None):
669        query = self._create_query(query,
670                                   omit_excluded=omit_excluded,
671                                   sort=sort,
672                                   exclude_tags=exclude_tags)
673        return query.count_threads()
674
675    def status_string(self):
676        raise NotImplementedError
677
678    def __repr__(self):
679        return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
680
681
682class AtomicContext:
683    """Context manager for atomic support.
684
685    This supports the notmuch_database_begin_atomic and
686    notmuch_database_end_atomic API calls.  The object can not be
687    directly instantiated by the user, only via ``Database.atomic``.
688    It does keep a reference to the :class:`Database` instance to keep
689    the C memory alive.
690
691    :raises XapianError: When this is raised at enter time the atomic
692       section is not active.  When it is raised at exit time the
693       atomic section is still active and you may need to try using
694       :meth:`force_end`.
695    :raises ObjectDestroyedError: if used after destroyed.
696    """
697
698    def __init__(self, db, ptr_name):
699        self._db = db
700        self._ptr = lambda: getattr(db, ptr_name)
701        self._exit_fn = lambda: None
702
703    def __del__(self):
704        self._destroy()
705
706    @property
707    def alive(self):
708        return self.parent.alive
709
710    def _destroy(self):
711        pass
712
713    def __enter__(self):
714        ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
715        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
716            raise errors.NotmuchError(ret)
717        self._exit_fn = self._end_atomic
718        return self
719
720    def _end_atomic(self):
721        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
722        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
723            raise errors.NotmuchError(ret)
724
725    def __exit__(self, exc_type, exc_value, traceback):
726        self._exit_fn()
727
728    def force_end(self):
729        """Force ending the atomic section.
730
731        This can only be called once __exit__ has been called.  It
732        will attempt to close the atomic section (again).  This is
733        useful if the original exit raised an exception and the atomic
734        section is still open.  But things are pretty ugly by now.
735
736        :raises XapianError: If exiting fails, the atomic section is
737           not ended.
738        :raises UnbalancedAtomicError: If the database was currently
739           not in an atomic section.
740        :raises ObjectDestroyedError: if used after destroyed.
741        """
742        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
743        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
744            raise errors.NotmuchError(ret)
745
746    def abort(self):
747        """Abort the transaction.
748
749        Aborting a transaction will not commit any of the changes, but
750        will also implicitly close the database.
751        """
752        self._exit_fn = lambda: None
753        self._db.close()
754
755
756@functools.total_ordering
757class DbRevision:
758    """A database revision.
759
760    The database revision number increases monotonically with each
761    commit to the database.  Which means user-visible changes can be
762    ordered.  This object is sortable with other revisions.  It
763    carries the UUID of the database to ensure it is only ever
764    compared with revisions from the same database.
765    """
766
767    def __init__(self, rev, uuid):
768        self._rev = rev
769        self._uuid = uuid
770
771    @property
772    def rev(self):
773        """The revision number, a positive integer."""
774        return self._rev
775
776    @property
777    def uuid(self):
778        """The UUID of the database, consider this opaque."""
779        return self._uuid
780
781    def __eq__(self, other):
782        if isinstance(other, self.__class__):
783            if self.uuid != other.uuid:
784                return False
785            return self.rev == other.rev
786        else:
787            return NotImplemented
788
789    def __lt__(self, other):
790        if self.__class__ is other.__class__:
791            if self.uuid != other.uuid:
792                return False
793            return self.rev < other.rev
794        else:
795            return NotImplemented
796
797    def __repr__(self):
798        return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
799
800
801class IndexOptions(base.NotmuchObject):
802    """Indexing options.
803
804    This represents the indexing options which can be used to index a
805    message.  See :meth:`Database.default_indexopts` to create an
806    instance of this.  It can be used e.g. when indexing a new message
807    using :meth:`Database.add`.
808    """
809    _opts_p = base.MemoryPointer()
810
811    def __init__(self, parent, opts_p):
812        self._parent = parent
813        self._opts_p = opts_p
814
815    @property
816    def alive(self):
817        if not self._parent.alive:
818            return False
819        try:
820            self._opts_p
821        except errors.ObjectDestroyedError:
822            return False
823        else:
824            return True
825
826    def _destroy(self):
827        if self.alive:
828            capi.lib.notmuch_indexopts_destroy(self._opts_p)
829        self._opts_p = None
830
831    @property
832    def decrypt_policy(self):
833        """The decryption policy.
834
835        This is an enum from the :class:`DecryptionPolicy`.  See the
836        `index.decrypt` section in :man:`notmuch-config` for details
837        on the options.  **Do not set this to
838        :attr:`DecryptionPolicy.TRUE`** without considering the
839        security of your index.
840
841        You can change this policy by assigning a new
842        :class:`DecryptionPolicy` to this property.
843
844        :raises ObjectDestroyedError: if used after destroyed.
845
846        :returns: A :class:`DecryptionPolicy` enum instance.
847        """
848        raw = capi.lib.notmuch_indexopts_get_decrypt_policy(self._opts_p)
849        return DecryptionPolicy(raw)
850
851    @decrypt_policy.setter
852    def decrypt_policy(self, val):
853        ret = capi.lib.notmuch_indexopts_set_decrypt_policy(
854            self._opts_p, val.value)
855        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
856            raise errors.NotmuchError(ret, msg)
857