3770b13299d1af3cf4626b126c5eccbc01293911
[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: str
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: str
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         
533         # create query, return None if too little mem available
534         query_p = Query._create(db.db_p, querystr)
535         if query_p is None:
536             NotmuchError(STATUS.NULL_POINTER)
537         self._query = query_p
538
539     def set_sort(self, sort):
540         """Set the sort order future results will be delivered in
541
542         Wraps the underlying *notmuch_query_set_sort* function.
543
544         :param sort: Sort order (see :attr:`Query.SORT`)
545         :returns: Nothing
546         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
547                     been initialized.
548         """
549         if self._query is None:
550             raise NotmuchError(STATUS.NOT_INITIALIZED)
551
552         self.sort = sort
553         nmlib.notmuch_query_set_sort(self._query, sort)
554
555     def search_threads(self):
556         """Execute a query for threads
557
558         Execute a query for threads, returning a :class:`Threads` iterator.
559         The returned threads are owned by the query and as such, will only be 
560         valid until the Query is deleted.
561
562         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
563         match the query. The method :meth:`Message.get_flag` allows us
564         to get the value of this flag.
565
566         Technically, it wraps the underlying
567         *notmuch_query_search_threads* function.
568
569         :returns: :class:`Threads`
570         :exception: :exc:`NotmuchError`
571
572                       * STATUS.NOT_INITIALIZED if query is not inited
573                       * STATUS.NULL_POINTER if search_threads failed 
574         """
575         if self._query is None:
576             raise NotmuchError(STATUS.NOT_INITIALIZED)            
577
578         threads_p = Query._search_threads(self._query)
579
580         if threads_p is None:
581             NotmuchError(STATUS.NULL_POINTER)
582
583         return Threads(threads_p,self)
584
585     def search_messages(self):
586         """Filter messages according to the query and return
587         :class:`Messages` in the defined sort order
588
589         Technically, it wraps the underlying
590         *notmuch_query_search_messages* function.
591
592         :returns: :class:`Messages`
593         :exception: :exc:`NotmuchError`
594
595                       * STATUS.NOT_INITIALIZED if query is not inited
596                       * STATUS.NULL_POINTER if search_messages failed 
597         """
598         if self._query is None:
599             raise NotmuchError(STATUS.NOT_INITIALIZED)            
600
601         msgs_p = Query._search_messages(self._query)
602
603         if msgs_p is None:
604             NotmuchError(STATUS.NULL_POINTER)
605
606         return Messages(msgs_p,self)
607
608     def count_messages(self):
609         """Estimate the number of messages matching the query
610
611         This function performs a search and returns Xapian's best
612         guess as to the number of matching messages. It is much faster
613         than performing :meth:`search_messages` and counting the
614         result with `len()` (although it always returned the same
615         result in my tests). Technically, it wraps the underlying
616         *notmuch_query_count_messages* function.
617
618         :returns: :class:`Messages`
619         :exception: :exc:`NotmuchError`
620
621                       * STATUS.NOT_INITIALIZED if query is not inited
622         """
623         if self._query is None:
624             raise NotmuchError(STATUS.NOT_INITIALIZED)            
625
626         return Query._count_messages(self._query)
627
628     def __del__(self):
629         """Close and free the Query"""
630         if self._query is not None:
631             nmlib.notmuch_query_destroy (self._query)
632
633
634 #------------------------------------------------------------------------------
635 class Directory(object):
636     """Represents a directory entry in the notmuch directory
637
638     Modifying attributes of this object will modify the
639     database, not the real directory attributes.
640
641     The Directory object is usually derived from another object
642     e.g. via :meth:`Database.get_directory`, and will automatically be
643     become invalid whenever that parent is deleted. You should
644     therefore initialized this object handing it a reference to the
645     parent, preventing the parent from automatically being garbage
646     collected.
647     """
648
649     """notmuch_directory_get_mtime"""
650     _get_mtime = nmlib.notmuch_directory_get_mtime
651     _get_mtime.restype = c_long
652
653     """notmuch_directory_set_mtime"""
654     _set_mtime = nmlib.notmuch_directory_set_mtime
655     _set_mtime.argtypes = [c_char_p, c_long]
656
657     """notmuch_directory_get_child_files"""
658     _get_child_files = nmlib.notmuch_directory_get_child_files
659     _get_child_files.restype = c_void_p
660
661     """notmuch_directory_get_child_directories"""
662     _get_child_directories = nmlib.notmuch_directory_get_child_directories
663     _get_child_directories.restype = c_void_p
664
665     def _verify_dir_initialized(self):
666         """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None"""
667         if self._dir_p is None:
668             raise NotmuchError(STATUS.NOT_INITIALIZED)            
669
670     def __init__(self, path, dir_p, parent):
671         """
672         :param path:   The absolute path of the directory object.
673         :param dir_p:  The pointer to an internal notmuch_directory_t object.
674         :param parent: The object this Directory is derived from
675                        (usually a :class:`Database`). We do not directly use
676                        this, but store a reference to it as long as
677                        this Directory object lives. This keeps the
678                        parent object alive.
679         """
680         self._path = path
681         self._dir_p = dir_p
682         self._parent = parent
683
684
685     def set_mtime (self, mtime):
686         """Sets the mtime value of this directory in the database
687
688         The intention is for the caller to use the mtime to allow efficient
689         identification of new messages to be added to the database. The
690         recommended usage is as follows:
691
692         * Read the mtime of a directory from the filesystem
693  
694         * Call :meth:`Database.add_message` for all mail files in
695           the directory
696
697         * Call notmuch_directory_set_mtime with the mtime read from the 
698           filesystem.  Then, when wanting to check for updates to the
699           directory in the future, the client can call :meth:`get_mtime`
700           and know that it only needs to add files if the mtime of the 
701           directory and files are newer than the stored timestamp.
702
703           .. note:: :meth:`get_mtime` function does not allow the caller 
704                  to distinguish a timestamp of 0 from a non-existent
705                  timestamp. So don't store a timestamp of 0 unless you are
706                  comfortable with that.  
707
708           :param mtime: A (time_t) timestamp 
709           :returns: Nothing on success, raising an exception on failure.
710           :exception: :exc:`NotmuchError`:
711
712                         STATUS.XAPIAN_EXCEPTION
713                           A Xapian exception occurred, mtime not stored.
714                         STATUS.READ_ONLY_DATABASE
715                           Database was opened in read-only mode so directory 
716                           mtime cannot be modified.
717                         STATUS.NOT_INITIALIZED
718                           The directory has not been initialized
719         """
720         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
721         self._verify_dir_initialized()
722
723         #TODO: make sure, we convert the mtime parameter to a 'c_long'
724         status = Directory._set_mtime(self._dir_p, mtime)
725
726         #return on success
727         if status == STATUS.SUCCESS:
728             return
729         #fail with Exception otherwise
730         raise NotmuchError(status)
731
732     def get_mtime (self):
733         """Gets the mtime value of this directory in the database
734
735         Retrieves a previously stored mtime for this directory.
736
737         :param mtime: A (time_t) timestamp 
738         :returns: Nothing on success, raising an exception on failure.
739         :exception: :exc:`NotmuchError`:
740
741                         STATUS.NOT_INITIALIZED
742                           The directory has not been initialized
743         """
744         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None
745         self._verify_dir_initialized()
746
747         return Directory._get_mtime (self._dir_p)
748
749     # Make mtime attribute a property of Directory()
750     mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
751                      and setting of the Directory *mtime* (read-write)
752
753                      See :meth:`get_mtime` and :meth:`set_mtime` for usage and 
754                      possible exceptions.""")
755
756     def get_child_files(self):
757         """Gets a Filenames iterator listing all the filenames of
758         messages in the database within the given directory.
759  
760         The returned filenames will be the basename-entries only (not
761         complete paths.
762         """
763         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
764         self._verify_dir_initialized()
765
766         files_p = Directory._get_child_files(self._dir_p)
767         return Filenames(files_p, self)
768
769     def get_child_directories(self):
770         """Gets a :class:`Filenames` iterator listing all the filenames of
771         sub-directories in the database within the given directory
772         
773         The returned filenames will be the basename-entries only (not
774         complete paths.
775         """
776         #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
777         self._verify_dir_initialized()
778
779         files_p = Directory._get_child_directories(self._dir_p)
780         return Filenames(files_p, self)
781
782     @property
783     def path(self):
784         """Returns the absolute path of this Directory (read-only)"""
785         return self._path
786
787     def __repr__(self):
788         """Object representation"""
789         return "<notmuch Directory object '%s'>" % self._path
790
791     def __del__(self):
792         """Close and free the Directory"""
793         if self._dir_p is not None:
794             nmlib.notmuch_directory_destroy(self._dir_p)
795
796 #------------------------------------------------------------------------------
797 class Filenames(object):
798     """An iterator over File- or Directory names that are stored in the database
799     """
800
801     #notmuch_filenames_get
802     _get = nmlib.notmuch_filenames_get
803     _get.restype = c_char_p
804
805     def __init__(self, files_p, parent):
806         """
807         :param files_p: The pointer to an internal notmuch_filenames_t object.
808         :param parent: The object this Directory is derived from
809                        (usually a Directory()). We do not directly use
810                        this, but store a reference to it as long as
811                        this Directory object lives. This keeps the
812                        parent object alive.
813         """
814         self._files_p = files_p
815         self._parent = parent
816
817     def __iter__(self):
818         """ Make Filenames an iterator """
819         return self
820
821     def next(self):
822         if self._files_p is None:
823             raise NotmuchError(STATUS.NOT_INITIALIZED)
824
825         if not nmlib.notmuch_filenames_valid(self._files_p):
826             self._files_p = None
827             raise StopIteration
828
829         file = Filenames._get (self._files_p)
830         nmlib.notmuch_filenames_move_to_next(self._files_p)
831         return file
832
833     def __len__(self):
834         """len(:class:`Filenames`) returns the number of contained files
835
836         .. note:: As this iterates over the files, we will not be able to 
837                iterate over them again! So this will fail::
838
839                  #THIS FAILS
840                  files = Database().get_directory('').get_child_files()
841                  if len(files) > 0:              #this 'exhausts' msgs
842                      # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
843                      for file in files: print file
844         """
845         if self._files_p is None:
846             raise NotmuchError(STATUS.NOT_INITIALIZED)
847
848         i=0
849         while nmlib.notmuch_filenames_valid(self._files_p):
850             nmlib.notmuch_filenames_move_to_next(self._files_p)
851             i += 1
852         self._files_p = None
853         return i
854
855     def __del__(self):
856         """Close and free Filenames"""
857         if self._files_p is not None:
858             nmlib.notmuch_filenames_destroy(self._files_p)