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