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