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