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