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