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