]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/database.py
python: Bulletproof Database() path parameter
[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 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):
265         """Adds a new message to the database
266
267         `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         If the message contains Maildir flags, we will -depending on the
278         notmuch configuration- sync those tags to initial notmuch tags.
279
280         :returns: On success, we return 
281
282            1) a :class:`Message` object that can be used for things
283               such as adding tags to the just-added message.
284            2) one of the following STATUS values:
285
286               STATUS.SUCCESS
287                   Message successfully added to database.
288               STATUS.DUPLICATE_MESSAGE_ID
289                   Message has the same message ID as another message already
290                   in the database. The new filename was successfully added
291                   to the message in the database.
292
293         :rtype:   2-tuple(:class:`Message`, STATUS)
294
295         :exception: Raises a :exc:`NotmuchError` with the following meaning.
296               If such an exception occurs, nothing was added to the database.
297
298               STATUS.FILE_ERROR
299                       An error occurred trying to open the file, (such as 
300                       permission denied, or file not found, etc.).
301               STATUS.FILE_NOT_EMAIL
302                       The contents of filename don't look like an email message.
303               STATUS.READ_ONLY_DATABASE
304                       Database was opened in read-only mode so no message can
305                       be added.
306               STATUS.NOT_INITIALIZED
307                       The database has not been initialized.
308         """
309         # Raise a NotmuchError if not initialized
310         self._verify_initialized_db()
311
312         msg_p = c_void_p()
313         status = nmlib.notmuch_database_add_message(self._db,
314                                                   filename,
315                                                   byref(msg_p))
316  
317         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
318             raise NotmuchError(status)
319
320         #construct Message(), sync initial tags from Maildir flags and return
321         msg = Message(msg_p, self)
322         msg.maildir_flags_to_tags()
323         return (msg, status)
324
325     def remove_message(self, filename):
326         """Removes a message from the given notmuch database
327
328         Note that only this particular filename association is removed from
329         the database. If the same message (as determined by the message ID)
330         is still available via other filenames, then the message will
331         persist in the database for those filenames. When the last filename
332         is removed for a particular message, the database content for that
333         message will be entirely removed.
334
335         :returns: A STATUS value with the following meaning:
336
337              STATUS.SUCCESS
338                The last filename was removed and the message was removed 
339                from the database.
340              STATUS.DUPLICATE_MESSAGE_ID
341                This filename was removed but the message persists in the 
342                database with at least one other filename.
343
344         :exception: Raises a :exc:`NotmuchError` with the following meaning.
345              If such an exception occurs, nothing was removed from the database.
346
347              STATUS.READ_ONLY_DATABASE
348                Database was opened in read-only mode so no message can be 
349                removed.
350              STATUS.NOT_INITIALIZED
351                The database has not been initialized.
352         """
353         # Raise a NotmuchError if not initialized
354         self._verify_initialized_db()
355
356         return nmlib.notmuch_database_remove_message(self._db,
357                                                        filename)
358
359     def find_message(self, msgid):
360         """Returns a :class:`Message` as identified by its message ID
361
362         Wraps the underlying *notmuch_database_find_message* function.
363
364         :param msgid: The message ID
365         :type msgid: string
366         :returns: :class:`Message` or `None` if no message is found or
367                   if any xapian exception or out-of-memory situation
368                   occurs. Do note that Xapian Exceptions include
369                   "Database modified" situations, e.g. when the
370                   notmuch database has been modified by
371                   another program in the meantime. A return value of 
372                   `None` is therefore no guarantee that the message 
373                   does not exist.
374         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
375                   the database was not intitialized.
376         """
377         # Raise a NotmuchError if not initialized
378         self._verify_initialized_db()
379
380         msg_p = Database._find_message(self._db, msgid)
381         if msg_p is None:
382             return None
383         return Message(msg_p, self)
384
385     def get_all_tags(self):
386         """Returns :class:`Tags` with a list of all tags found in the database
387
388         :returns: :class:`Tags`
389         :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
390         """
391         # Raise a NotmuchError if not initialized
392         self._verify_initialized_db()
393
394         tags_p = Database._get_all_tags (self._db)
395         if tags_p == None:
396             raise NotmuchError(STATUS.NULL_POINTER)
397         return Tags(tags_p, self)
398
399     def create_query(self, querystring):
400         """Returns a :class:`Query` derived from this database
401
402         This is a shorthand method for doing::
403
404           # short version
405           # Automatically frees the Database() when 'q' is deleted
406
407           q  = Database(dbpath).create_query('from:"Biene Maja"')
408
409           # long version, which is functionally equivalent but will keep the
410           # Database in the 'db' variable around after we delete 'q':
411
412           db = Database(dbpath)
413           q  = Query(db,'from:"Biene Maja"')
414
415         This function is a python extension and not in the underlying C API.
416         """
417         # Raise a NotmuchError if not initialized
418         self._verify_initialized_db()
419
420         return Query(self, querystring)
421
422     def __repr__(self):
423         return "'Notmuch DB " + self.get_path() + "'"
424
425     def __del__(self):
426         """Close and free the notmuch database if needed"""
427         if self._db is not None:
428             nmlib.notmuch_database_close(self._db)
429
430     def _get_user_default_db(self):
431         """ Reads a user's notmuch config and returns his db location
432
433         Throws a NotmuchError if it cannot find it"""
434         from ConfigParser import SafeConfigParser
435         config = SafeConfigParser()
436         conf_f = os.getenv('NOTMUCH_CONFIG',
437                            os.path.expanduser('~/.notmuch-config'))
438         config.read(conf_f)
439         if not config.has_option('database','path'):
440             raise NotmuchError(message=
441                                "No DB path specified and no user default found")
442         return config.get('database','path')
443
444     @property
445     def db_p(self):
446         """Property returning a pointer to `notmuch_database_t` or `None`
447
448         This should normally not be needed by a user (and is not yet
449         guaranteed to remain stable in future versions).
450         """
451         return self._db
452
453 #------------------------------------------------------------------------------
454 class Query(object):
455     """Represents a search query on an opened :class:`Database`.
456
457     A query selects and filters a subset of messages from the notmuch
458     database we derive from.
459
460     Query() provides an instance attribute :attr:`sort`, which
461     contains the sort order (if specified via :meth:`set_sort`) or
462     `None`.
463
464     Technically, it wraps the underlying *notmuch_query_t* struct.
465
466     .. note:: Do remember that as soon as we tear down this object,
467            all underlying derived objects such as threads,
468            messages, tags etc will be freed by the underlying library
469            as well. Accessing these objects will lead to segfaults and
470            other unexpected behavior. See above for more details.
471     """
472     # constants
473     SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID', 'UNSORTED'])
474     """Constants: Sort order in which to return results"""
475
476     """notmuch_query_create"""
477     _create = nmlib.notmuch_query_create
478     _create.restype = c_void_p
479
480     """notmuch_query_search_threads"""
481     _search_threads = nmlib.notmuch_query_search_threads
482     _search_threads.restype = c_void_p
483
484     """notmuch_query_search_messages"""
485     _search_messages = nmlib.notmuch_query_search_messages
486     _search_messages.restype = c_void_p
487
488
489     """notmuch_query_count_messages"""
490     _count_messages = nmlib.notmuch_query_count_messages
491     _count_messages.restype = c_uint
492
493     def __init__(self, db, querystr):
494         """
495         :param db: An open database which we derive the Query from.
496         :type db: :class:`Database`
497         :param querystr: The query string for the message.
498         :type querystr: str
499         """
500         self._db = None
501         self._query = None
502         self.sort = None
503         self.create(db, querystr)
504
505     def create(self, db, querystr):
506         """Creates a new query derived from a Database
507
508         This function is utilized by __init__() and usually does not need to 
509         be called directly.
510
511         :param db: Database to create the query from.
512         :type db: :class:`Database`
513         :param querystr: The query string
514         :type querystr: str
515         :returns: Nothing
516         :exception: :exc:`NotmuchError`
517
518                       * STATUS.NOT_INITIALIZED if db is not inited
519                       * STATUS.NULL_POINTER if the query creation failed 
520                         (too little memory)
521         """
522         if db.db_p is None:
523             raise NotmuchError(STATUS.NOT_INITIALIZED)            
524         # create reference to parent db to keep it alive
525         self._db = db
526         
527         # create query, return None if too little mem available
528         query_p = Query._create(db.db_p, querystr)
529         if query_p is None:
530             NotmuchError(STATUS.NULL_POINTER)
531         self._query = query_p
532
533     def set_sort(self, sort):
534         """Set the sort order future results will be delivered in
535
536         Wraps the underlying *notmuch_query_set_sort* function.
537
538         :param sort: Sort order (see :attr:`Query.SORT`)
539         :returns: Nothing
540         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
541                     been initialized.
542         """
543         if self._query is None:
544             raise NotmuchError(STATUS.NOT_INITIALIZED)
545
546         self.sort = sort
547         nmlib.notmuch_query_set_sort(self._query, sort)
548
549     def search_threads(self):
550         """Execute a query for threads
551
552         Execute a query for threads, returning a :class:`Threads` iterator.
553         The returned threads are owned by the query and as such, will only be 
554         valid until the Query is deleted.
555
556         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
557         match the query. The method :meth:`Message.get_flag` allows us
558         to get the value of this flag.
559
560         Technically, it wraps the underlying
561         *notmuch_query_search_threads* function.
562
563         :returns: :class:`Threads`
564         :exception: :exc:`NotmuchError`
565
566                       * STATUS.NOT_INITIALIZED if query is not inited
567                       * STATUS.NULL_POINTER if search_threads failed 
568         """
569         if self._query is None:
570             raise NotmuchError(STATUS.NOT_INITIALIZED)            
571
572         threads_p = Query._search_threads(self._query)
573
574         if threads_p is None:
575             NotmuchError(STATUS.NULL_POINTER)
576
577         return Threads(threads_p,self)
578
579     def search_messages(self):
580         """Filter messages according to the query and return
581         :class:`Messages` in the defined sort order
582
583         Technically, it wraps the underlying
584         *notmuch_query_search_messages* function.
585
586         :returns: :class:`Messages`
587         :exception: :exc:`NotmuchError`
588
589                       * STATUS.NOT_INITIALIZED if query is not inited
590                       * STATUS.NULL_POINTER if search_messages failed 
591         """
592         if self._query is None:
593             raise NotmuchError(STATUS.NOT_INITIALIZED)            
594
595         msgs_p = Query._search_messages(self._query)
596
597         if msgs_p is None:
598             NotmuchError(STATUS.NULL_POINTER)
599
600         return Messages(msgs_p,self)
601
602     def count_messages(self):
603         """Estimate the number of messages matching the query
604
605         This function performs a search and returns Xapian's best
606         guess as to the number of matching messages. It is much faster
607         than performing :meth:`search_messages` and counting the
608         result with `len()` (although it always returned the same
609         result in my tests). Technically, it wraps the underlying
610         *notmuch_query_count_messages* function.
611
612         :returns: :class:`Messages`
613         :exception: :exc:`NotmuchError`
614
615                       * STATUS.NOT_INITIALIZED if query is not inited
616         """
617         if self._query is None:
618             raise NotmuchError(STATUS.NOT_INITIALIZED)            
619
620         return Query._count_messages(self._query)
621
622     def __del__(self):
623         """Close and free the Query"""
624         if self._query is not None:
625             nmlib.notmuch_query_destroy (self._query)
626
627
628 #------------------------------------------------------------------------------
629 class Directory(object):
630     """Represents a directory entry in the notmuch directory
631
632     Modifying attributes of this object will modify the
633     database, not the real directory attributes.
634
635     The Directory object is usually derived from another object
636     e.g. via :meth:`Database.get_directory`, and will automatically be
637     become invalid whenever that parent is deleted. You should
638     therefore initialized this object handing it a reference to the
639     parent, preventing the parent from automatically being garbage
640     collected.
641     """
642
643     """notmuch_directory_get_mtime"""
644     _get_mtime = nmlib.notmuch_directory_get_mtime
645     _get_mtime.restype = c_long
646
647     """notmuch_directory_set_mtime"""
648     _set_mtime = nmlib.notmuch_directory_set_mtime
649     _set_mtime.argtypes = [c_char_p, c_long]
650
651     """notmuch_directory_get_child_files"""
652     _get_child_files = nmlib.notmuch_directory_get_child_files
653     _get_child_files.restype = c_void_p
654
655     """notmuch_directory_get_child_directories"""
656     _get_child_directories = nmlib.notmuch_directory_get_child_directories
657     _get_child_directories.restype = c_void_p
658
659     def _verify_dir_initialized(self):
660         """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None"""
661         if self._dir_p is None:
662             raise NotmuchError(STATUS.NOT_INITIALIZED)            
663
664     def __init__(self, path, dir_p, parent):
665         """
666         :param path:   The absolute path of the directory object.
667         :param dir_p:  The pointer to an internal notmuch_directory_t object.
668         :param parent: The object this Directory is derived from
669                        (usually a :class:`Database`). We do not directly use
670                        this, but store a reference to it as long as
671                        this Directory object lives. This keeps the
672                        parent object alive.
673         """
674         self._path = path
675         self._dir_p = dir_p
676         self._parent = parent
677
678
679     def set_mtime (self, mtime):
680         """Sets the mtime value of this directory in the database
681
682         The intention is for the caller to use the mtime to allow efficient
683         identification of new messages to be added to the database. The
684         recommended usage is as follows:
685
686         * Read the mtime of a directory from the filesystem
687  
688         * Call :meth:`Database.add_message` for all mail files in
689           the directory
690
691         * Call notmuch_directory_set_mtime with the mtime read from the 
692           filesystem.  Then, when wanting to check for updates to the
693           directory in the future, the client can call :meth:`get_mtime`
694           and know that it only needs to add files if the mtime of the 
695           directory and files are newer than the stored timestamp.
696
697           .. note:: :meth:`get_mtime` function does not allow the caller 
698                  to distinguish a timestamp of 0 from a non-existent
699                  timestamp. So don't store a timestamp of 0 unless you are
700                  comfortable with that.  
701
702           :param mtime: A (time_t) timestamp 
703           :returns: Nothing on success, raising an exception on failure.
704           :exception: :exc:`NotmuchError`:
705
706                         STATUS.XAPIAN_EXCEPTION
707                           A Xapian exception occurred, mtime not stored.
708                         STATUS.READ_ONLY_DATABASE
709                           Database was opened in read-only mode so directory 
710                           mtime cannot be modified.
711                         STATUS.NOT_INITIALIZED
712                           The directory has not been initialized
713         """
714         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
715         self._verify_dir_initialized()
716
717         #TODO: make sure, we convert the mtime parameter to a 'c_long'
718         status = Directory._set_mtime(self._dir_p, mtime)
719
720         #return on success
721         if status == STATUS.SUCCESS:
722             return
723         #fail with Exception otherwise
724         raise NotmuchError(status)
725
726     def get_mtime (self):
727         """Gets the mtime value of this directory in the database
728
729         Retrieves a previously stored mtime for this directory.
730
731         :param mtime: A (time_t) timestamp 
732         :returns: Nothing on success, raising an exception on failure.
733         :exception: :exc:`NotmuchError`:
734
735                         STATUS.NOT_INITIALIZED
736                           The directory has not been initialized
737         """
738         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None
739         self._verify_dir_initialized()
740
741         return Directory._get_mtime (self._dir_p)
742
743     # Make mtime attribute a property of Directory()
744     mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
745                      and setting of the Directory *mtime* (read-write)
746
747                      See :meth:`get_mtime` and :meth:`set_mtime` for usage and 
748                      possible exceptions.""")
749
750     def get_child_files(self):
751         """Gets a Filenames iterator listing all the filenames of
752         messages in the database within the given directory.
753  
754         The returned filenames will be the basename-entries only (not
755         complete paths.
756         """
757         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
758         self._verify_dir_initialized()
759
760         files_p = Directory._get_child_files(self._dir_p)
761         return Filenames(files_p, self)
762
763     def get_child_directories(self):
764         """Gets a :class:`Filenames` iterator listing all the filenames of
765         sub-directories in the database within the given directory
766         
767         The returned filenames will be the basename-entries only (not
768         complete paths.
769         """
770         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
771         self._verify_dir_initialized()
772
773         files_p = Directory._get_child_directories(self._dir_p)
774         return Filenames(files_p, self)
775
776     @property
777     def path(self):
778         """Returns the absolute path of this Directory (read-only)"""
779         return self._path
780
781     def __repr__(self):
782         """Object representation"""
783         return "<notmuch Directory object '%s'>" % self._path
784
785     def __del__(self):
786         """Close and free the Directory"""
787         if self._dir_p is not None:
788             nmlib.notmuch_directory_destroy(self._dir_p)
789
790 #------------------------------------------------------------------------------
791 class Filenames(object):
792     """An iterator over File- or Directory names that are stored in the database
793     """
794
795     #notmuch_filenames_get
796     _get = nmlib.notmuch_filenames_get
797     _get.restype = c_char_p
798
799     def __init__(self, files_p, parent):
800         """
801         :param files_p: The pointer to an internal notmuch_filenames_t object.
802         :param parent: The object this Directory is derived from
803                        (usually a Directory()). We do not directly use
804                        this, but store a reference to it as long as
805                        this Directory object lives. This keeps the
806                        parent object alive.
807         """
808         self._files_p = files_p
809         self._parent = parent
810
811     def __iter__(self):
812         """ Make Filenames an iterator """
813         return self
814
815     def next(self):
816         if self._files_p is None:
817             raise NotmuchError(STATUS.NOT_INITIALIZED)
818
819         if not nmlib.notmuch_filenames_valid(self._files_p):
820             self._files_p = None
821             raise StopIteration
822
823         file = Filenames._get (self._files_p)
824         nmlib.notmuch_filenames_move_to_next(self._files_p)
825         return file
826
827     def __len__(self):
828         """len(:class:`Filenames`) returns the number of contained files
829
830         .. note:: As this iterates over the files, we will not be able to 
831                iterate over them again! So this will fail::
832
833                  #THIS FAILS
834                  files = Database().get_directory('').get_child_files()
835                  if len(files) > 0:              #this 'exhausts' msgs
836                      # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
837                      for file in files: print file
838         """
839         if self._files_p is None:
840             raise NotmuchError(STATUS.NOT_INITIALIZED)
841
842         i=0
843         while nmlib.notmuch_filenames_valid(self._files_p):
844             nmlib.notmuch_filenames_move_to_next(self._files_p)
845             i += 1
846         self._files_p = None
847         return i
848
849     def __del__(self):
850         """Close and free Filenames"""
851         if self._files_p is not None:
852             nmlib.notmuch_filenames_destroy(self._files_p)