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