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