]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/database.py
python: provide a Database.close function
[notmuch] / bindings / python / notmuch / database.py
1 """
2 This file is part of notmuch.
3
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18 """
19
20 import os
21 import codecs
22 from ctypes import c_char_p, c_void_p, c_uint, c_long, byref, POINTER
23 from notmuch.globals import (nmlib, STATUS, NotmuchError, NotInitializedError,
24      NullPointerError, Enum, _str,
25      NotmuchDatabaseP, NotmuchDirectoryP, NotmuchMessageP, NotmuchTagsP,
26      NotmuchQueryP, NotmuchMessagesP, NotmuchThreadsP, NotmuchFilenamesP)
27 from notmuch.thread import Threads
28 from notmuch.message import Messages, Message
29 from notmuch.tag import Tags
30
31
32 class Database(object):
33     """The :class:`Database` is the highest-level object that notmuch
34     provides. It references a notmuch database, and can be opened in
35     read-only or read-write mode. A :class:`Query` can be derived from
36     or be applied to a specific database to find messages. Also adding
37     and removing messages to the database happens via this
38     object. Modifications to the database are not atmic by default (see
39     :meth:`begin_atomic`) and once a database has been modified, all
40     other database objects pointing to the same data-base will throw an
41     :exc:`XapianError` as the underlying database has been
42     modified. Close and reopen the database to continue working with it.
43
44     .. note::
45
46         Any function in this class can and will throw an
47         :exc:`NotInitializedError` if the database was not intitialized
48         properly.
49
50     .. note::
51
52         Do remember that as soon as we tear down (e.g. via `del db`) this
53         object, all underlying derived objects such as queries, threads,
54         messages, tags etc will be freed by the underlying library as well.
55         Accessing these objects will lead to segfaults and other unexpected
56         behavior. See above for more details.
57     """
58     _std_db_path = None
59     """Class attribute to cache user's default database"""
60
61     MODE = Enum(['READ_ONLY', 'READ_WRITE'])
62     """Constants: Mode in which to open the database"""
63
64     """notmuch_database_get_directory"""
65     _get_directory = nmlib.notmuch_database_get_directory
66     _get_directory.argtypes = [NotmuchDatabaseP, c_char_p]
67     _get_directory.restype = NotmuchDirectoryP
68
69     """notmuch_database_get_path"""
70     _get_path = nmlib.notmuch_database_get_path
71     _get_path.argtypes = [NotmuchDatabaseP]
72     _get_path.restype = c_char_p
73
74     """notmuch_database_get_version"""
75     _get_version = nmlib.notmuch_database_get_version
76     _get_version.argtypes = [NotmuchDatabaseP]
77     _get_version.restype = c_uint
78
79     """notmuch_database_open"""
80     _open = nmlib.notmuch_database_open
81     _open.argtypes = [c_char_p, c_uint]
82     _open.restype = NotmuchDatabaseP
83
84     """notmuch_database_upgrade"""
85     _upgrade = nmlib.notmuch_database_upgrade
86     _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
87     _upgrade.restype = c_uint
88
89     """ notmuch_database_find_message"""
90     _find_message = nmlib.notmuch_database_find_message
91     _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
92                               POINTER(NotmuchMessageP)]
93     _find_message.restype = c_uint
94
95     """notmuch_database_find_message_by_filename"""
96     _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
97     _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
98                                           POINTER(NotmuchMessageP)]
99     _find_message_by_filename.restype = c_uint
100
101     """notmuch_database_get_all_tags"""
102     _get_all_tags = nmlib.notmuch_database_get_all_tags
103     _get_all_tags.argtypes = [NotmuchDatabaseP]
104     _get_all_tags.restype = NotmuchTagsP
105
106     """notmuch_database_create"""
107     _create = nmlib.notmuch_database_create
108     _create.argtypes = [c_char_p]
109     _create.restype = NotmuchDatabaseP
110
111     def __init__(self, path=None, create=False, mode=0):
112         """If *path* is `None`, we will try to read a users notmuch
113         configuration and use his configured database. The location of the
114         configuration file can be specified through the environment variable
115         *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
116
117         If *create* is `True`, the database will always be created in
118         :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
119
120         :param path:   Directory to open/create the database in (see
121                        above for behavior if `None`)
122         :type path:    `str` or `None`
123         :param create: Pass `False` to open an existing, `True` to create a new
124                        database.
125         :type create:  bool
126         :param mode:   Mode to open a database in. Is always
127                        :attr:`MODE`.READ_WRITE when creating a new one.
128         :type mode:    :attr:`MODE`
129         :exception: :exc:`NotmuchError` or derived exception in case of
130             failure.
131         """
132         self._db = None
133         if path is None:
134             # no path specified. use a user's default database
135             if Database._std_db_path is None:
136                 #the following line throws a NotmuchError if it fails
137                 Database._std_db_path = self._get_user_default_db()
138             path = Database._std_db_path
139
140         if create == False:
141             self.open(path, mode)
142         else:
143             self.create(path)
144
145     def __del__(self):
146         self.close()
147
148     def _assert_db_is_initialized(self):
149         """Raises :exc:`NotInitializedError` if self._db is `None`"""
150         if self._db is None:
151             raise NotInitializedError()
152
153     def create(self, path):
154         """Creates a new notmuch database
155
156         This function is used by __init__() and usually does not need
157         to be called directly. It wraps the underlying
158         *notmuch_database_create* function and creates a new notmuch
159         database at *path*. It will always return a database in :attr:`MODE`
160         .READ_WRITE mode as creating an empty database for
161         reading only does not make a great deal of sense.
162
163         :param path: A directory in which we should create the database.
164         :type path: str
165         :returns: Nothing
166         :exception: :exc:`NotmuchError` in case of any failure
167                     (possibly after printing an error message on stderr).
168         """
169         if self._db is not None:
170             raise NotmuchError(message="Cannot create db, this Database() "
171                                        "already has an open one.")
172
173         res = Database._create(_str(path), Database.MODE.READ_WRITE)
174
175         if not res:
176             raise NotmuchError(
177                 message="Could not create the specified database")
178         self._db = res
179
180     def open(self, path, mode=0):
181         """Opens an existing database
182
183         This function is used by __init__() and usually does not need
184         to be called directly. It wraps the underlying
185         *notmuch_database_open* function.
186
187         :param status: Open the database in read-only or read-write mode
188         :type status:  :attr:`MODE`
189         :returns: Nothing
190         :exception: Raises :exc:`NotmuchError` in case of any failure
191                     (possibly after printing an error message on stderr).
192         """
193         res = Database._open(_str(path), mode)
194
195         if not res:
196             raise NotmuchError(message="Could not open the specified database")
197         self._db = res
198
199     _close = nmlib.notmuch_database_close
200     _close.argtypes = [NotmuchDatabaseP]
201     _close.restype = None
202
203     def close(self):
204         """Close and free the notmuch database if needed"""
205         if self._db is not None:
206             self._close(self._db)
207             self._db = None
208
209     def get_path(self):
210         """Returns the file path of an open database"""
211         self._assert_db_is_initialized()
212         return Database._get_path(self._db).decode('utf-8')
213
214     def get_version(self):
215         """Returns the database format version
216
217         :returns: The database version as positive integer
218         """
219         self._assert_db_is_initialized()
220         return Database._get_version(self._db)
221
222     _needs_upgrade = nmlib.notmuch_database_needs_upgrade
223     _needs_upgrade.argtypes = [NotmuchDatabaseP]
224     _needs_upgrade.restype = bool
225
226     def needs_upgrade(self):
227         """Does this database need to be upgraded before writing to it?
228
229         If this function returns `True` then no functions that modify the
230         database (:meth:`add_message`,
231         :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
232         etc.) will work unless :meth:`upgrade` is called successfully first.
233
234         :returns: `True` or `False`
235         """
236         self._assert_db_is_initialized()
237         return self._needs_upgrade(self._db)
238
239     def upgrade(self):
240         """Upgrades the current database
241
242         After opening a database in read-write mode, the client should
243         check if an upgrade is needed (notmuch_database_needs_upgrade) and
244         if so, upgrade with this function before making any modifications.
245
246         NOT IMPLEMENTED: The optional progress_notify callback can be
247         used by the caller to provide progress indication to the
248         user. If non-NULL it will be called periodically with
249         'progress' as a floating-point value in the range of [0.0..1.0]
250         indicating the progress made so far in the upgrade process.
251
252         :TODO: catch exceptions, document return values and etc...
253         """
254         self._assert_db_is_initialized()
255         status = Database._upgrade(self._db, None, None)
256         #TODO: catch exceptions, document return values and etc
257         return status
258
259     _begin_atomic = nmlib.notmuch_database_begin_atomic
260     _begin_atomic.argtypes = [NotmuchDatabaseP]
261     _begin_atomic.restype = c_uint
262
263     def begin_atomic(self):
264         """Begin an atomic database operation
265
266         Any modifications performed between a successful
267         :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
268         the database atomically.  Note that, unlike a typical database
269         transaction, this only ensures atomicity, not durability;
270         neither begin nor end necessarily flush modifications to disk.
271
272         :returns: :attr:`STATUS`.SUCCESS or raises
273         :exception: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
274                     Xapian exception occurred; atomic section not entered.
275
276         *Added in notmuch 0.9*"""
277         self._assert_db_is_initialized()
278         status = self._begin_atomic(self._db)
279         if status != STATUS.SUCCESS:
280             raise NotmuchError(status)
281         return status
282
283     _end_atomic = nmlib.notmuch_database_end_atomic
284     _end_atomic.argtypes = [NotmuchDatabaseP]
285     _end_atomic.restype = c_uint
286
287     def end_atomic(self):
288         """Indicate the end of an atomic database operation
289
290         See :meth:`begin_atomic` for details.
291
292         :returns: :attr:`STATUS`.SUCCESS or raises
293
294         :exception:
295             :exc:`NotmuchError`:
296                 :attr:`STATUS`.XAPIAN_EXCEPTION
297                     A Xapian exception occurred; atomic section not
298                     ended.
299                 :attr:`STATUS`.UNBALANCED_ATOMIC:
300                     end_atomic has been called more times than begin_atomic.
301
302         *Added in notmuch 0.9*"""
303         self._assert_db_is_initialized()
304         status = self._end_atomic(self._db)
305         if status != STATUS.SUCCESS:
306             raise NotmuchError(status)
307         return status
308
309     def get_directory(self, path):
310         """Returns a :class:`Directory` of path,
311         (creating it if it does not exist(?))
312
313         .. warning::
314
315             This call needs a writeable database in
316             :attr:`Database.MODE`.READ_WRITE mode. The underlying library will
317             exit the program if this method is used on a read-only database!
318
319         :param path: An unicode string containing the path relative to the path
320               of database (see :meth:`get_path`), or else should be an absolute
321               path with initial components that match the path of 'database'.
322         :returns: :class:`Directory` or raises an exception.
323         :exception:
324             :exc:`NotmuchError` with :attr:`STATUS`.FILE_ERROR
325                     If path is not relative database or absolute with initial
326                     components same as database.
327         """
328         self._assert_db_is_initialized()
329         # sanity checking if path is valid, and make path absolute
330         if path[0] == os.sep:
331             # we got an absolute path
332             if not path.startswith(self.get_path()):
333                 # but its initial components are not equal to the db path
334                 raise NotmuchError(STATUS.FILE_ERROR,
335                                    message="Database().get_directory() called "
336                                            "with a wrong absolute path.")
337             abs_dirpath = path
338         else:
339             #we got a relative path, make it absolute
340             abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
341
342         dir_p = Database._get_directory(self._db, _str(path))
343
344         # return the Directory, init it with the absolute path
345         return Directory(_str(abs_dirpath), dir_p, self)
346
347     _add_message = nmlib.notmuch_database_add_message
348     _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
349                              POINTER(NotmuchMessageP)]
350     _add_message.restype = c_uint
351
352     def add_message(self, filename, sync_maildir_flags=False):
353         """Adds a new message to the database
354
355         :param filename: should be a path relative to the path of the
356             open database (see :meth:`get_path`), or else should be an
357             absolute filename with initial components that match the
358             path of the database.
359
360             The file should be a single mail message (not a
361             multi-message mbox) that is expected to remain at its
362             current location, since the notmuch database will reference
363             the filename, and will not copy the entire contents of the
364             file.
365
366         :param sync_maildir_flags: If the message contains Maildir
367             flags, we will -depending on the notmuch configuration- sync
368             those tags to initial notmuch tags, if set to `True`. It is
369             `False` by default to remain consistent with the libnotmuch
370             API. You might want to look into the underlying method
371             :meth:`Message.maildir_flags_to_tags`.
372
373         :returns: On success, we return
374
375            1) a :class:`Message` object that can be used for things
376               such as adding tags to the just-added message.
377            2) one of the following :attr:`STATUS` values:
378
379               :attr:`STATUS`.SUCCESS
380                   Message successfully added to database.
381               :attr:`STATUS`.DUPLICATE_MESSAGE_ID
382                   Message has the same message ID as another message already
383                   in the database. The new filename was successfully added
384                   to the list of the filenames for the existing message.
385
386         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
387
388         :exception: Raises a :exc:`NotmuchError` with the following meaning.
389               If such an exception occurs, nothing was added to the database.
390
391               :attr:`STATUS`.FILE_ERROR
392                       An error occurred trying to open the file, (such as
393                       permission denied, or file not found, etc.).
394               :attr:`STATUS`.FILE_NOT_EMAIL
395                       The contents of filename don't look like an email
396                       message.
397               :attr:`STATUS`.READ_ONLY_DATABASE
398                       Database was opened in read-only mode so no message can
399                       be added.
400         """
401         self._assert_db_is_initialized()
402         msg_p = NotmuchMessageP()
403         status = self._add_message(self._db, _str(filename), byref(msg_p))
404
405         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
406             raise NotmuchError(status)
407
408         #construct Message() and return
409         msg = Message(msg_p, self)
410         #automatic sync initial tags from Maildir flags
411         if sync_maildir_flags:
412             msg.maildir_flags_to_tags()
413         return (msg, status)
414
415     _remove_message = nmlib.notmuch_database_remove_message
416     _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
417     _remove_message.restype = c_uint
418
419     def remove_message(self, filename):
420         """Removes a message (filename) from the given notmuch database
421
422         Note that only this particular filename association is removed from
423         the database. If the same message (as determined by the message ID)
424         is still available via other filenames, then the message will
425         persist in the database for those filenames. When the last filename
426         is removed for a particular message, the database content for that
427         message will be entirely removed.
428
429         :returns: A :attr:`STATUS` value with the following meaning:
430
431              :attr:`STATUS`.SUCCESS
432                The last filename was removed and the message was removed
433                from the database.
434              :attr:`STATUS`.DUPLICATE_MESSAGE_ID
435                This filename was removed but the message persists in the
436                database with at least one other filename.
437
438         :exception: Raises a :exc:`NotmuchError` with the following meaning.
439              If such an exception occurs, nothing was removed from the
440              database.
441
442              :attr:`STATUS`.READ_ONLY_DATABASE
443                Database was opened in read-only mode so no message can be
444                removed.
445         """
446         self._assert_db_is_initialized()
447         return self._remove_message(self._db, _str(filename))
448
449     def find_message(self, msgid):
450         """Returns a :class:`Message` as identified by its message ID
451
452         Wraps the underlying *notmuch_database_find_message* function.
453
454         :param msgid: The message ID
455         :type msgid: unicode or str
456         :returns: :class:`Message` or `None` if no message is found.
457         :exception:
458             :exc:`OutOfMemoryError`
459                   If an Out-of-memory occured while constructing the message.
460             :exc:`XapianError`
461                   In case of a Xapian Exception. These exceptions
462                   include "Database modified" situations, e.g. when the
463                   notmuch database has been modified by another program
464                   in the meantime. In this case, you should close and
465                   reopen the database and retry.
466             :exc:`NotInitializedError` if
467                     the database was not intitialized.
468         """
469         self._assert_db_is_initialized()
470         msg_p = NotmuchMessageP()
471         status = Database._find_message(self._db, _str(msgid), byref(msg_p))
472         if status != STATUS.SUCCESS:
473             raise NotmuchError(status)
474         return msg_p and Message(msg_p, self) or None
475
476     def find_message_by_filename(self, filename):
477         """Find a message with the given filename
478
479         .. warning::
480
481             This call needs a writeable database in
482             :attr:`Database.MODE`.READ_WRITE mode. The underlying library will
483             exit the program if this method is used on a read-only database!
484
485         :returns: If the database contains a message with the given
486             filename, then a class:`Message:` is returned.  This
487             function returns None if no message is found with the given
488             filename.
489
490         :exception:
491             :exc:`OutOfMemoryError`
492                   If an Out-of-memory occured while constructing the message.
493             :exc:`XapianError`
494                   In case of a Xapian Exception. These exceptions
495                   include "Database modified" situations, e.g. when the
496                   notmuch database has been modified by another program
497                   in the meantime. In this case, you should close and
498                   reopen the database and retry.
499             :exc:`NotInitializedError` if
500                     the database was not intitialized.
501
502         *Added in notmuch 0.9*"""
503         self._assert_db_is_initialized()
504         msg_p = NotmuchMessageP()
505         status = Database._find_message_by_filename(self._db, _str(filename),
506                                                     byref(msg_p))
507         if status != STATUS.SUCCESS:
508             raise NotmuchError(status)
509         return msg_p and Message(msg_p, self) or None
510
511     def get_all_tags(self):
512         """Returns :class:`Tags` with a list of all tags found in the database
513
514         :returns: :class:`Tags`
515         :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
516                     on error
517         """
518         self._assert_db_is_initialized()
519         tags_p = Database._get_all_tags(self._db)
520         if tags_p == None:
521             raise NotmuchError(STATUS.NULL_POINTER)
522         return Tags(tags_p, self)
523
524     def create_query(self, querystring):
525         """Returns a :class:`Query` derived from this database
526
527         This is a shorthand method for doing::
528
529           # short version
530           # Automatically frees the Database() when 'q' is deleted
531
532           q  = Database(dbpath).create_query('from:"Biene Maja"')
533
534           # long version, which is functionally equivalent but will keep the
535           # Database in the 'db' variable around after we delete 'q':
536
537           db = Database(dbpath)
538           q  = Query(db,'from:"Biene Maja"')
539
540         This function is a python extension and not in the underlying C API.
541         """
542         return Query(self, querystring)
543
544     def __repr__(self):
545         return "'Notmuch DB " + self.get_path() + "'"
546
547     def _get_user_default_db(self):
548         """ Reads a user's notmuch config and returns his db location
549
550         Throws a NotmuchError if it cannot find it"""
551         try:
552             # python3.x
553             from configparser import SafeConfigParser
554         except ImportError:
555             # python2.x
556             from ConfigParser import SafeConfigParser
557
558         config = SafeConfigParser()
559         conf_f = os.getenv('NOTMUCH_CONFIG',
560                            os.path.expanduser('~/.notmuch-config'))
561         config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
562         if not config.has_option('database', 'path'):
563             raise NotmuchError(message="No DB path specified"
564                                        " and no user default found")
565         return config.get('database', 'path')
566
567     @property
568     def db_p(self):
569         """Property returning a pointer to `notmuch_database_t` or `None`
570
571         This should normally not be needed by a user (and is not yet
572         guaranteed to remain stable in future versions).
573         """
574         return self._db
575
576
577 class Query(object):
578     """Represents a search query on an opened :class:`Database`.
579
580     A query selects and filters a subset of messages from the notmuch
581     database we derive from.
582
583     :class:`Query` provides an instance attribute :attr:`sort`, which
584     contains the sort order (if specified via :meth:`set_sort`) or
585     `None`.
586
587     Any function in this class may throw an :exc:`NotInitializedError`
588     in case the underlying query object was not set up correctly.
589
590     .. note:: Do remember that as soon as we tear down this object,
591            all underlying derived objects such as threads,
592            messages, tags etc will be freed by the underlying library
593            as well. Accessing these objects will lead to segfaults and
594            other unexpected behavior. See above for more details.
595     """
596     # constants
597     SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
598     """Constants: Sort order in which to return results"""
599
600     """notmuch_query_create"""
601     _create = nmlib.notmuch_query_create
602     _create.argtypes = [NotmuchDatabaseP, c_char_p]
603     _create.restype = NotmuchQueryP
604
605     """notmuch_query_search_threads"""
606     _search_threads = nmlib.notmuch_query_search_threads
607     _search_threads.argtypes = [NotmuchQueryP]
608     _search_threads.restype = NotmuchThreadsP
609
610     """notmuch_query_search_messages"""
611     _search_messages = nmlib.notmuch_query_search_messages
612     _search_messages.argtypes = [NotmuchQueryP]
613     _search_messages.restype = NotmuchMessagesP
614
615     """notmuch_query_count_messages"""
616     _count_messages = nmlib.notmuch_query_count_messages
617     _count_messages.argtypes = [NotmuchQueryP]
618     _count_messages.restype = c_uint
619
620     def __init__(self, db, querystr):
621         """
622         :param db: An open database which we derive the Query from.
623         :type db: :class:`Database`
624         :param querystr: The query string for the message.
625         :type querystr: utf-8 encoded str or unicode
626         """
627         self._db = None
628         self._query = None
629         self.sort = None
630         self.create(db, querystr)
631
632     def _assert_query_is_initialized(self):
633         """Raises :exc:`NotInitializedError` if self._query is `None`"""
634         if self._query is None:
635             raise NotInitializedError()
636
637     def create(self, db, querystr):
638         """Creates a new query derived from a Database
639
640         This function is utilized by __init__() and usually does not need to
641         be called directly.
642
643         :param db: Database to create the query from.
644         :type db: :class:`Database`
645         :param querystr: The query string
646         :type querystr: utf-8 encoded str or unicode
647         :returns: Nothing
648         :exception:
649             :exc:`NullPointerError` if the query creation failed
650                 (e.g. too little memory).
651             :exc:`NotInitializedError` if the underlying db was not
652                 intitialized.
653         """
654         db._assert_db_is_initialized()
655         # create reference to parent db to keep it alive
656         self._db = db
657         # create query, return None if too little mem available
658         query_p = Query._create(db.db_p, _str(querystr))
659         if not query_p:
660             raise NullPointerError
661         self._query = query_p
662
663     _set_sort = nmlib.notmuch_query_set_sort
664     _set_sort.argtypes = [NotmuchQueryP, c_uint]
665     _set_sort.argtypes = None
666
667     def set_sort(self, sort):
668         """Set the sort order future results will be delivered in
669
670         :param sort: Sort order (see :attr:`Query.SORT`)
671         """
672         self._assert_query_is_initialized()
673         self.sort = sort
674         self._set_sort(self._query, sort)
675
676     def search_threads(self):
677         """Execute a query for threads
678
679         Execute a query for threads, returning a :class:`Threads` iterator.
680         The returned threads are owned by the query and as such, will only be
681         valid until the Query is deleted.
682
683         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
684         match the query. The method :meth:`Message.get_flag` allows us
685         to get the value of this flag.
686
687         :returns: :class:`Threads`
688         :exception: :exc:`NullPointerError` if search_threads failed
689         """
690         self._assert_query_is_initialized()
691         threads_p = Query._search_threads(self._query)
692
693         if not threads_p:
694             raise NullPointerError
695         return Threads(threads_p, self)
696
697     def search_messages(self):
698         """Filter messages according to the query and return
699         :class:`Messages` in the defined sort order
700
701         :returns: :class:`Messages`
702         :exception: :exc:`NullPointerError` if search_messages failed
703         """
704         self._assert_query_is_initialized()
705         msgs_p = Query._search_messages(self._query)
706
707         if not msgs_p:
708             raise NullPointerError
709         return Messages(msgs_p, self)
710
711     def count_messages(self):
712         """Estimate the number of messages matching the query
713
714         This function performs a search and returns Xapian's best
715         guess as to the number of matching messages. It is much faster
716         than performing :meth:`search_messages` and counting the
717         result with `len()` (although it always returned the same
718         result in my tests). Technically, it wraps the underlying
719         *notmuch_query_count_messages* function.
720
721         :returns: :class:`Messages`
722         """
723         self._assert_query_is_initialized()
724         return Query._count_messages(self._query)
725
726     _destroy = nmlib.notmuch_query_destroy
727     _destroy.argtypes = [NotmuchQueryP]
728     _destroy.restype = None
729
730     def __del__(self):
731         """Close and free the Query"""
732         if self._query is not None:
733             self._destroy(self._query)
734
735
736 class Directory(object):
737     """Represents a directory entry in the notmuch directory
738
739     Modifying attributes of this object will modify the
740     database, not the real directory attributes.
741
742     The Directory object is usually derived from another object
743     e.g. via :meth:`Database.get_directory`, and will automatically be
744     become invalid whenever that parent is deleted. You should
745     therefore initialized this object handing it a reference to the
746     parent, preventing the parent from automatically being garbage
747     collected.
748     """
749
750     """notmuch_directory_get_mtime"""
751     _get_mtime = nmlib.notmuch_directory_get_mtime
752     _get_mtime.argtypes = [NotmuchDirectoryP]
753     _get_mtime.restype = c_long
754
755     """notmuch_directory_set_mtime"""
756     _set_mtime = nmlib.notmuch_directory_set_mtime
757     _set_mtime.argtypes = [NotmuchDirectoryP, c_long]
758     _set_mtime.restype = c_uint
759
760     """notmuch_directory_get_child_files"""
761     _get_child_files = nmlib.notmuch_directory_get_child_files
762     _get_child_files.argtypes = [NotmuchDirectoryP]
763     _get_child_files.restype = NotmuchFilenamesP
764
765     """notmuch_directory_get_child_directories"""
766     _get_child_directories = nmlib.notmuch_directory_get_child_directories
767     _get_child_directories.argtypes = [NotmuchDirectoryP]
768     _get_child_directories.restype = NotmuchFilenamesP
769
770     def _assert_dir_is_initialized(self):
771         """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
772         if dir_p is None"""
773         if not self._dir_p:
774             raise NotmuchError(STATUS.NOT_INITIALIZED)
775
776     def __init__(self, path, dir_p, parent):
777         """
778         :param path:   The absolute path of the directory object as unicode.
779         :param dir_p:  The pointer to an internal notmuch_directory_t object.
780         :param parent: The object this Directory is derived from
781                        (usually a :class:`Database`). We do not directly use
782                        this, but store a reference to it as long as
783                        this Directory object lives. This keeps the
784                        parent object alive.
785         """
786         assert isinstance(path, unicode), "Path needs to be an UNICODE object"
787         self._path = path
788         self._dir_p = dir_p
789         self._parent = parent
790
791     def set_mtime(self, mtime):
792         """Sets the mtime value of this directory in the database
793
794         The intention is for the caller to use the mtime to allow efficient
795         identification of new messages to be added to the database. The
796         recommended usage is as follows:
797
798         * Read the mtime of a directory from the filesystem
799
800         * Call :meth:`Database.add_message` for all mail files in
801           the directory
802
803         * Call notmuch_directory_set_mtime with the mtime read from the
804           filesystem.  Then, when wanting to check for updates to the
805           directory in the future, the client can call :meth:`get_mtime`
806           and know that it only needs to add files if the mtime of the
807           directory and files are newer than the stored timestamp.
808
809           .. note::
810
811                 :meth:`get_mtime` function does not allow the caller to
812                 distinguish a timestamp of 0 from a non-existent timestamp. So
813                 don't store a timestamp of 0 unless you are comfortable with
814                 that.
815
816           :param mtime: A (time_t) timestamp
817           :returns: Nothing on success, raising an exception on failure.
818           :exception: :exc:`NotmuchError`:
819
820                         :attr:`STATUS`.XAPIAN_EXCEPTION
821                           A Xapian exception occurred, mtime not stored.
822                         :attr:`STATUS`.READ_ONLY_DATABASE
823                           Database was opened in read-only mode so directory
824                           mtime cannot be modified.
825                         :attr:`STATUS`.NOT_INITIALIZED
826                           The directory has not been initialized
827         """
828         self._assert_dir_is_initialized()
829         #TODO: make sure, we convert the mtime parameter to a 'c_long'
830         status = Directory._set_mtime(self._dir_p, mtime)
831
832         #return on success
833         if status == STATUS.SUCCESS:
834             return
835         #fail with Exception otherwise
836         raise NotmuchError(status)
837
838     def get_mtime(self):
839         """Gets the mtime value of this directory in the database
840
841         Retrieves a previously stored mtime for this directory.
842
843         :param mtime: A (time_t) timestamp
844         :returns: Nothing on success, raising an exception on failure.
845         :exception: :exc:`NotmuchError`:
846
847                         :attr:`STATUS`.NOT_INITIALIZED
848                           The directory has not been initialized
849         """
850         self._assert_dir_is_initialized()
851         return Directory._get_mtime(self._dir_p)
852
853     # Make mtime attribute a property of Directory()
854     mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
855                      and setting of the Directory *mtime* (read-write)
856
857                      See :meth:`get_mtime` and :meth:`set_mtime` for usage and
858                      possible exceptions.""")
859
860     def get_child_files(self):
861         """Gets a Filenames iterator listing all the filenames of
862         messages in the database within the given directory.
863
864         The returned filenames will be the basename-entries only (not
865         complete paths.
866         """
867         self._assert_dir_is_initialized()
868         files_p = Directory._get_child_files(self._dir_p)
869         return Filenames(files_p, self)
870
871     def get_child_directories(self):
872         """Gets a :class:`Filenames` iterator listing all the filenames of
873         sub-directories in the database within the given directory
874
875         The returned filenames will be the basename-entries only (not
876         complete paths.
877         """
878         self._assert_dir_is_initialized()
879         files_p = Directory._get_child_directories(self._dir_p)
880         return Filenames(files_p, self)
881
882     @property
883     def path(self):
884         """Returns the absolute path of this Directory (read-only)"""
885         return self._path
886
887     def __repr__(self):
888         """Object representation"""
889         return "<notmuch Directory object '%s'>" % self._path
890
891     _destroy = nmlib.notmuch_directory_destroy
892     _destroy.argtypes = [NotmuchDirectoryP]
893     _destroy.argtypes = None
894
895     def __del__(self):
896         """Close and free the Directory"""
897         if self._dir_p is not None:
898             self._destroy(self._dir_p)
899
900
901 class Filenames(object):
902     """An iterator over File- or Directory names stored in the database"""
903
904     #notmuch_filenames_get
905     _get = nmlib.notmuch_filenames_get
906     _get.argtypes = [NotmuchFilenamesP]
907     _get.restype = c_char_p
908
909     def __init__(self, files_p, parent):
910         """
911         :param files_p: The pointer to an internal notmuch_filenames_t object.
912         :param parent: The object this Directory is derived from
913                        (usually a Directory()). We do not directly use
914                        this, but store a reference to it as long as
915                        this Directory object lives. This keeps the
916                        parent object alive.
917         """
918         self._files_p = files_p
919         self._parent = parent
920
921     def __iter__(self):
922         """ Make Filenames an iterator """
923         return self
924
925     _valid = nmlib.notmuch_filenames_valid
926     _valid.argtypes = [NotmuchFilenamesP]
927     _valid.restype = bool
928
929     _move_to_next = nmlib.notmuch_filenames_move_to_next
930     _move_to_next.argtypes = [NotmuchFilenamesP]
931     _move_to_next.restype = None
932
933     def __next__(self):
934         if not self._files_p:
935             raise NotmuchError(STATUS.NOT_INITIALIZED)
936
937         if not self._valid(self._files_p):
938             self._files_p = None
939             raise StopIteration
940
941         file_ = Filenames._get(self._files_p)
942         self._move_to_next(self._files_p)
943         return file_.decode('utf-8', 'ignore')
944     next = __next__ # python2.x iterator protocol compatibility
945
946     def __len__(self):
947         """len(:class:`Filenames`) returns the number of contained files
948
949         .. note::
950
951             As this iterates over the files, we will not be able to
952             iterate over them again! So this will fail::
953
954                  #THIS FAILS
955                  files = Database().get_directory('').get_child_files()
956                  if len(files) > 0:  # this 'exhausts' msgs
957                      # next line raises
958                      # NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
959                      for file in files: print file
960         """
961         if not self._files_p:
962             raise NotmuchError(STATUS.NOT_INITIALIZED)
963
964         i = 0
965         while self._valid(self._files_p):
966             self._move_to_next(self._files_p)
967             i += 1
968         self._files_p = None
969         return i
970
971     _destroy = nmlib.notmuch_filenames_destroy
972     _destroy.argtypes = [NotmuchFilenamesP]
973     _destroy.restype = None
974
975     def __del__(self):
976         """Close and free Filenames"""
977         if self._files_p is not None:
978             self._destroy(self._files_p)