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