]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/database.py
setup.py and MANIFEST.in for the 0.1 release. Unfortunately post-version tag.
[notmuch] / cnotmuch / database.py
1 import ctypes, os
2 from ctypes import c_int, c_char_p, c_void_p, c_uint, c_uint64, c_bool, byref
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
4 import logging
5 from datetime import date
6
7 class Database(object):
8     """Represents a notmuch database (wraps notmuch_database_t)
9
10     .. note:: Do remember that as soon as we tear down this object,
11            all underlying derived objects such as queries, threads,
12            messages, tags etc will be freed by the underlying library
13            as well. Accessing these objects will lead to segfaults and
14            other unexpected behavior. See above for more details.
15     """
16     _std_db_path = None
17     """Class attribute to cache user's default database"""
18
19     MODE = Enum(['READ_ONLY','READ_WRITE'])
20     """Constants: Mode in which to open the database"""
21
22     """notmuch_database_get_path (notmuch_database_t *database)"""
23     _get_path = nmlib.notmuch_database_get_path
24     _get_path.restype = c_char_p
25
26     """notmuch_database_get_version"""
27     _get_version = nmlib.notmuch_database_get_version
28     _get_version.restype = c_uint
29
30     """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
31     _open = nmlib.notmuch_database_open 
32     _open.restype = c_void_p
33
34     """ notmuch_database_find_message """
35     _find_message = nmlib.notmuch_database_find_message
36     _find_message.restype = c_void_p
37
38     """notmuch_database_get_all_tags (notmuch_database_t *database)"""
39     _get_all_tags = nmlib.notmuch_database_get_all_tags
40     _get_all_tags.restype = c_void_p
41
42     """ notmuch_database_create(const char *path):"""
43     _create = nmlib.notmuch_database_create
44     _create.restype = c_void_p
45
46     def __init__(self, path=None, create=False, mode= 0):
47         """If *path* is *None*, we will try to read a users notmuch 
48         configuration and use his configured database. The location of the 
49         configuration file can be specified through the environment variable
50         *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
51
52         If *create* is `True`, the database will always be created in
53         :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
54
55         :param path:   Directory to open/create the database in (see
56                        above for behavior if `None`)
57         :type path:    `str` or `None`
58         :param create: Pass `False` to open an existing, `True` to create a new
59                        database.  
60         :type create:  bool
61         :param mode:   Mode to open a database in. Is always 
62                        :attr:`MODE`.READ_WRITE when creating a new one.
63         :type mode:    :attr:`MODE`
64         :returns:      Nothing
65         :exception:    :exc:`NotmuchError` in case of failure.
66         """
67         self._db = None
68         if path is None:
69             # no path specified. use a user's default database
70             if Database._std_db_path is None:
71                 #the following line throws a NotmuchError if it fails
72                 Database._std_db_path = self._get_user_default_db()
73             path = Database._std_db_path
74
75         if create == False:
76             self.open(path, mode)
77         else:
78             self.create(path)
79
80     def _verify_initialized_db(self):
81         """Raises a NotmuchError in case self._db is still None"""
82         if self._db is None:
83             raise NotmuchError(STATUS.NOT_INITIALIZED)            
84
85     def create(self, path):
86         """Creates a new notmuch database
87
88         This function is used by __init__() and usually does not need
89         to be called directly. It wraps the underlying
90         *notmuch_database_create* function and creates a new notmuch
91         database at *path*. It will always return a database in
92         :attr:`MODE`.READ_WRITE mode as creating an empty database for
93         reading only does not make a great deal of sense.
94
95         :param path: A directory in which we should create the database.
96         :type path: str
97         :returns: Nothing
98         :exception: :exc:`NotmuchError` in case of any failure
99                     (after printing an error message on stderr).
100         """
101         if self._db is not None:
102             raise NotmuchError(
103             message="Cannot create db, this Database() already has an open one.")
104
105         res = Database._create(path, Database.MODE.READ_WRITE)
106
107         if res is None:
108             raise NotmuchError(
109                 message="Could not create the specified database")
110         self._db = res
111
112     def open(self, path, mode= 0): 
113         """Opens an existing database
114
115         This function is used by __init__() and usually does not need
116         to be called directly. It wraps the underlying
117         *notmuch_database_open* function.
118
119         :param status: Open the database in read-only or read-write mode
120         :type status:  :attr:`MODE` 
121         :returns: Nothing
122         :exception: Raises :exc:`NotmuchError` in case
123                     of any failure (after printing an error message on stderr).
124         """
125
126         res = Database._open(path, mode)
127
128         if res is None:
129             raise NotmuchError(
130                 message="Could not open the specified database")
131         self._db = res
132
133     def get_path(self):
134         """Returns the file path of an open database
135
136         Wraps notmuch_database_get_path"""
137         # Raise a NotmuchError if not initialized
138         self._verify_initialized_db()
139
140         return Database._get_path(self._db)
141
142     def get_version(self):
143         """Returns the database format version
144
145         :returns: The database version as positive integer
146         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
147                     the database was not intitialized.
148         """
149         # Raise a NotmuchError if not initialized
150         self._verify_initialized_db()
151
152         return Database._get_version (self._db)
153
154     def needs_upgrade(self):
155         """Does this database need to be upgraded before writing to it?
156
157         If this function returns True then no functions that modify the
158         database (:meth:`add_message`, :meth:`add_tag`,
159         :meth:`Directory.set_mtime`, etc.) will work unless :meth:`upgrade` 
160         is called successfully first.
161
162         :returns: `True` or `False`
163         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
164                     the database was not intitialized.
165         """
166         # Raise a NotmuchError if not initialized
167         self._verify_initialized_db()
168
169         return notmuch_database_needs_upgrade(self.db) 
170
171     def add_message(self, filename):
172         """Adds a new message to the database
173
174         `filename` should be a path relative to the path of the open
175         database (see :meth:`get_path`), or else should be an absolute
176         filename with initial components that match the path of the
177         database.
178
179         The file should be a single mail message (not a multi-message mbox)
180         that is expected to remain at its current location, since the
181         notmuch database will reference the filename, and will not copy the
182         entire contents of the file.
183
184         :returns: On success, we return 
185
186            1) a :class:`Message` object that can be used for things
187               such as adding tags to the just-added message.
188            2) one of the following STATUS values:
189
190               STATUS.SUCCESS
191                   Message successfully added to database.
192               STATUS.DUPLICATE_MESSAGE_ID
193                   Message has the same message ID as another message already
194                   in the database. The new filename was successfully added
195                   to the message in the database.
196
197         :rtype:   2-tuple(:class:`Message`, STATUS)
198
199         :exception: Raises a :exc:`NotmuchError` with the following meaning.
200               If such an exception occurs, nothing was added to the database.
201
202               STATUS.FILE_ERROR
203                       An error occurred trying to open the file, (such as 
204                       permission denied, or file not found, etc.).
205               STATUS.FILE_NOT_EMAIL
206                       The contents of filename don't look like an email message.
207               STATUS.READ_ONLY_DATABASE
208                       Database was opened in read-only mode so no message can
209                       be added.
210               STATUS.NOT_INITIALIZED
211                       The database has not been initialized.
212         """
213         # Raise a NotmuchError if not initialized
214         self._verify_initialized_db()
215
216         msg_p = c_void_p()
217         status = nmlib.notmuch_database_add_message(self._db,
218                                                   filename,
219                                                   byref(msg_p))
220  
221         if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
222             raise NotmuchError(status)
223
224         #construct Message() and return
225         msg = Message(msg_p, self)
226         return (msg, status)
227
228     def remove_message(self, filename):
229         """Removes a message from the given notmuch database
230
231         Note that only this particular filename association is removed from
232         the database. If the same message (as determined by the message ID)
233         is still available via other filenames, then the message will
234         persist in the database for those filenames. When the last filename
235         is removed for a particular message, the database content for that
236         message will be entirely removed.
237
238         :returns: A STATUS.* value with the following meaning:
239
240              STATUS.SUCCESS
241                The last filename was removed and the message was removed 
242                from the database.
243              STATUS.DUPLICATE_MESSAGE_ID
244                This filename was removed but the message persists in the 
245                database with at least one other filename.
246
247         :exception: Raises a :exc:`NotmuchError` with the following meaning.
248              If such an exception occurs, nothing was removed from the database.
249
250              STATUS.READ_ONLY_DATABASE
251                Database was opened in read-only mode so no message can be 
252                removed.
253              STATUS.NOT_INITIALIZED
254                The database has not been initialized.
255         """
256         # Raise a NotmuchError if not initialized
257         self._verify_initialized_db()
258
259         status = nmlib.notmuch_database_remove_message(self._db,
260                                                        filename)
261
262     def find_message(self, msgid):
263         """Returns a :class:`Message` as identified by its message ID
264
265         Wraps the underlying *notmuch_database_find_message* function.
266
267         :param msgid: The message ID
268         :type msgid: string
269         :returns: :class:`Message` or `None` if no message is found or if an
270                   out-of-memory situation occurs.
271         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
272                   the database was not intitialized.
273         """
274         # Raise a NotmuchError if not initialized
275         self._verify_initialized_db()
276
277         msg_p = Database._find_message(self._db, msgid)
278         if msg_p is None:
279             return None
280         return Message(msg_p, self)
281
282     def get_all_tags(self):
283         """Returns :class:`Tags` with a list of all tags found in the database
284
285         :returns: :class:`Tags`
286         :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
287         """
288         # Raise a NotmuchError if not initialized
289         self._verify_initialized_db()
290
291         tags_p = Database._get_all_tags (self._db)
292         if tags_p == None:
293             raise NotmuchError(STATUS.NULL_POINTER)
294         return Tags(tags_p, self)
295
296     def create_query(self, querystring):
297         """Returns a :class:`Query` derived from this database
298
299         This is a shorthand method for doing::
300
301           # short version
302           # Automatically frees the Database() when 'q' is deleted
303
304           q  = Database(dbpath).create_query('from:"Biene Maja"')
305
306           # long version, which is functionally equivalent but will keep the
307           # Database in the 'db' variable around after we delete 'q':
308
309           db = Database(dbpath)
310           q  = Query(db,'from:"Biene Maja"')
311
312         This function is a python extension and not in the underlying C API.
313         """
314         # Raise a NotmuchError if not initialized
315         self._verify_initialized_db()
316
317         return Query(self._db, querystring)
318
319     def __repr__(self):
320         return "'Notmuch DB " + self.get_path() + "'"
321
322     def __del__(self):
323         """Close and free the notmuch database if needed"""
324         if self._db is not None:
325             logging.debug("Freeing the database now")
326             nmlib.notmuch_database_close(self._db)
327
328     def _get_user_default_db(self):
329         """ Reads a user's notmuch config and returns his db location
330
331         Throws a NotmuchError if it cannot find it"""
332         from ConfigParser import SafeConfigParser
333         config = SafeConfigParser()
334         conf_f = os.getenv('NOTMUCH_CONFIG',
335                            os.path.expanduser('~/.notmuch-config'))
336         config.read(conf_f)
337         if not config.has_option('database','path'):
338             raise NotmuchError(message=
339                                "No DB path specified and no user default found")
340         return config.get('database','path')
341
342     @property
343     def db_p(self):
344         """Property returning a pointer to `notmuch_database_t` or `None`
345
346         This should normally not be needed by a user (and is not yet
347         guaranteed to remain stable in future versions).
348         """
349         return self._db
350
351 #------------------------------------------------------------------------------
352 class Query(object):
353     """Represents a search query on an opened :class:`Database`.
354
355     A query selects and filters a subset of messages from the notmuch
356     database we derive from.
357
358     Technically, it wraps the underlying *notmuch_query_t* struct.
359
360     .. note:: Do remember that as soon as we tear down this object,
361            all underlying derived objects such as threads,
362            messages, tags etc will be freed by the underlying library
363            as well. Accessing these objects will lead to segfaults and
364            other unexpected behavior. See above for more details.
365     """
366     # constants
367     SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
368     """Constants: Sort order in which to return results"""
369
370     """notmuch_query_create"""
371     _create = nmlib.notmuch_query_create
372     _create.restype = c_void_p
373
374     """notmuch_query_search_messages"""
375     _search_messages = nmlib.notmuch_query_search_messages
376     _search_messages.restype = c_void_p
377
378
379     """notmuch_query_count_messages"""
380     _count_messages = nmlib.notmuch_query_count_messages
381     _count_messages.restype = c_uint
382
383     def __init__(self, db, querystr):
384         """
385         :param db: An open database which we derive the Query from.
386         :type db: :class:`Database`
387         :param querystr: The query string for the message.
388         :type querystr: str
389         """
390         self._db = None
391         self._query = None
392         self.create(db, querystr)
393
394     def create(self, db, querystr):
395         """Creates a new query derived from a Database
396
397         This function is utilized by __init__() and usually does not need to 
398         be called directly.
399
400         :param db: Database to create the query from.
401         :type db: :class:`Database`
402         :param querystr: The query string
403         :type querystr: str
404         :returns: Nothing
405         :exception: :exc:`NotmuchError`
406
407                       * STATUS.NOT_INITIALIZED if db is not inited
408                       * STATUS.NULL_POINTER if the query creation failed 
409                         (too little memory)
410         """
411         if db.db_p is None:
412             raise NotmuchError(STATUS.NOT_INITIALIZED)            
413         # create reference to parent db to keep it alive
414         self._db = db
415         
416         # create query, return None if too little mem available
417         query_p = Query._create(db.db_p, querystr)
418         if query_p is None:
419             NotmuchError(STATUS.NULL_POINTER)
420         self._query = query_p
421
422     def set_sort(self, sort):
423         """Set the sort order future results will be delivered in
424
425         Wraps the underlying *notmuch_query_set_sort* function.
426
427         :param sort: Sort order (see :attr:`Query.SORT`)
428         :returns: Nothing
429         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
430                     been initialized.
431         """
432         if self._query is None:
433             raise NotmuchError(STATUS.NOT_INITIALIZED)
434
435         nmlib.notmuch_query_set_sort(self._query, sort)
436
437     def search_messages(self):
438         """Filter messages according to the query and return
439         :class:`Messages` in the defined sort order
440
441         Technically, it wraps the underlying
442         *notmuch_query_search_messages* function.
443
444         :returns: :class:`Messages`
445         :exception: :exc:`NotmuchError`
446
447                       * STATUS.NOT_INITIALIZED if query is not inited
448                       * STATUS.NULL_POINTER if search_messages failed 
449         """
450         if self._query is None:
451             raise NotmuchError(STATUS.NOT_INITIALIZED)            
452
453         msgs_p = Query._search_messages(self._query)
454
455         if msgs_p is None:
456             NotmuchError(STATUS.NULL_POINTER)
457
458         return Messages(msgs_p,self)
459
460     def count_messages(self):
461         """Estimate the number of messages matching the query
462
463         This function performs a search and returns Xapian's best
464         guess as to the number of matching messages. It is much faster
465         than performing :meth:`search_messages` and counting the
466         result with `len()` (although it always returned the same
467         result in my tests). Technically, it wraps the underlying
468         *notmuch_query_count_messages* function.
469
470         :returns: :class:`Messages`
471         :exception: :exc:`NotmuchError`
472
473                       * STATUS.NOT_INITIALIZED if query is not inited
474         """
475         if self._query is None:
476             raise NotmuchError(STATUS.NOT_INITIALIZED)            
477
478         return Query._count_messages(self._query)
479
480     def __del__(self):
481         """Close and free the Query"""
482         if self._query is not None:
483             logging.debug("Freeing the Query now")
484             nmlib.notmuch_query_destroy (self._query)
485
486 #------------------------------------------------------------------------------
487 class Tags(object):
488     """Represents a list of notmuch tags
489
490     This object provides an iterator over a list of notmuch tags. Do
491     note that the underlying library only provides a one-time iterator
492     (it cannot reset the iterator to the start). Thus iterating over
493     the function will "exhaust" the list of tags, and a subsequent
494     iteration attempt will raise a :exc:`NotmuchError`
495     STATUS.NOT_INITIALIZED. Also note, that any function that uses
496     iteration (nearly all) will also exhaust the tags. So both::
497
498       for tag in tags: print tag 
499
500     as well as::
501
502        number_of_tags = len(tags)
503
504     and even a simple::
505
506        #str() iterates over all tags to construct a space separated list
507        print(str(tags))
508
509     will "exhaust" the Tags. If you need to re-iterate over a list of
510     tags you will need to retrieve a new :class:`Tags` object.
511     """
512
513     #notmuch_tags_get
514     _get = nmlib.notmuch_tags_get
515     _get.restype = c_char_p
516
517     def __init__(self, tags_p, parent=None):
518         """
519         :param tags_p: A pointer to an underlying *notmuch_tags_t*
520              structure. These are not publically exposed, so a user
521              will almost never instantiate a :class:`Tags` object
522              herself. They are usually handed back as a result,
523              e.g. in :meth:`Database.get_all_tags`.  *tags_p* must be
524              valid, we will raise an :exc:`NotmuchError`
525              (STATUS.NULL_POINTER) if it is `None`.
526         :type tags_p: :class:`ctypes.c_void_p`
527         :param parent: The parent object (ie :class:`Database` or 
528              :class:`Message` these tags are derived from, and saves a
529              reference to it, so we can automatically delete the db object
530              once all derived objects are dead.
531         :TODO: Make the iterator optionally work more than once by
532                cache the tags in the Python object(?)
533         """
534         if tags_p is None:
535             NotmuchError(STATUS.NULL_POINTER)
536
537         self._tags = tags_p
538         #save reference to parent object so we keep it alive
539         self._parent = parent
540         logging.debug("Inited Tags derived from %s" %(repr(parent)))
541     
542     def __iter__(self):
543         """ Make Tags an iterator """
544         return self
545
546     def next(self):
547         if self._tags is None:
548             raise NotmuchError(STATUS.NOT_INITIALIZED)
549
550         if not nmlib.notmuch_tags_valid(self._tags):
551             self._tags = None
552             raise StopIteration
553
554         tag = Tags._get (self._tags)
555         nmlib.notmuch_tags_move_to_next(self._tags)
556         return tag
557
558     def __len__(self):
559         """len(:class:`Tags`) returns the number of contained tags
560
561         .. note:: As this iterates over the tags, we will not be able
562                to iterate over them again (as in retrieve them)! If
563                the tags have been exhausted already, this will raise a
564                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
565                subsequent attempts.
566         """
567         if self._tags is None:
568             raise NotmuchError(STATUS.NOT_INITIALIZED)
569
570         i=0
571         while nmlib.notmuch_tags_valid(self._msgs):
572             nmlib.notmuch_tags_move_to_next(self._msgs)
573             i += 1
574         self._tags = None
575         return i
576
577     def __str__(self):
578         """The str() representation of Tags() is a space separated list of tags
579
580         .. note:: As this iterates over the tags, we will not be able
581                to iterate over them again (as in retrieve them)! If
582                the tags have been exhausted already, this will raise a
583                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
584                subsequent attempts.
585         """
586         return " ".join(self)
587
588     def __del__(self):
589         """Close and free the notmuch tags"""
590         if self._tags is not None:
591             logging.debug("Freeing the Tags now")
592             nmlib.notmuch_tags_destroy (self._tags)
593
594
595 #------------------------------------------------------------------------------
596 class Messages(object):
597     """Represents a list of notmuch messages
598
599     This object provides an iterator over a list of notmuch messages
600     (Technically, it provides a wrapper for the underlying
601     *notmuch_messages_t* structure). Do note that the underlying
602     library only provides a one-time iterator (it cannot reset the
603     iterator to the start). Thus iterating over the function will
604     "exhaust" the list of messages, and a subsequent iteration attempt
605     will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
606     note, that any function that uses iteration will also
607     exhaust the messages. So both::
608
609       for msg in msgs: print msg 
610
611     as well as::
612
613        number_of_msgs = len(msgs)
614
615     will "exhaust" the Messages. If you need to re-iterate over a list of
616     messages you will need to retrieve a new :class:`Messages` object.
617
618     Things are not as bad as it seems though, you can store and reuse
619     the single Message objects as often as you want as long as you
620     keep the parent Messages object around. (Recall that due to
621     hierarchical memory allocation, all derived Message objects will
622     be invalid when we delete the parent Messages() object, even if it
623     was already "exhausted".) So this works::
624
625       db   = Database()
626       msgs = Query(db,'').search_messages() #get a Messages() object
627       msglist = []
628       for m in msgs:
629          msglist.append(m)
630
631       # msgs is "exhausted" now and even len(msgs) will raise an exception.
632       # However it will be kept around until all retrieved Message() objects are
633       # also deleted. If you did e.g. an explicit del(msgs) here, the 
634       # following lines would fail.
635       
636       # You can reiterate over *msglist* however as often as you want. 
637       # It is simply a list with Message objects.
638
639       print (msglist[0].get_filename())
640       print (msglist[1].get_filename())
641       print (msglist[0].get_message_id())
642     """
643
644     #notmuch_tags_get
645     _get = nmlib.notmuch_messages_get
646     _get.restype = c_void_p
647
648     _collect_tags = nmlib.notmuch_messages_collect_tags
649     _collect_tags.restype = c_void_p
650
651     def __init__(self, msgs_p, parent=None):
652         """
653         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
654              structure. These are not publically exposed, so a user
655              will almost never instantiate a :class:`Messages` object
656              herself. They are usually handed back as a result,
657              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
658              valid, we will raise an :exc:`NotmuchError`
659              (STATUS.NULL_POINTER) if it is `None`.
660         :type msgs_p: :class:`ctypes.c_void_p`
661         :param parent: The parent object
662              (ie :class:`Query`) these tags are derived from. It saves
663              a reference to it, so we can automatically delete the db
664              object once all derived objects are dead.
665         :TODO: Make the iterator work more than once and cache the tags in 
666                the Python object.(?)
667         """
668         if msgs_p is None:
669             NotmuchError(STATUS.NULL_POINTER)
670
671         self._msgs = msgs_p
672         #store parent, so we keep them alive as long as self  is alive
673         self._parent = parent
674         logging.debug("Inited Messages derived from %s" %(str(parent)))
675
676     def collect_tags(self):
677         """Return the unique :class:`Tags` in the contained messages
678
679         :returns: :class:`Tags`
680         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
681
682         .. note:: :meth:`collect_tags` will iterate over the messages and
683           therefore will not allow further iterations.
684         """
685         if self._msgs is None:
686             raise NotmuchError(STATUS.NOT_INITIALIZED)
687
688         # collect all tags (returns NULL on error)
689         tags_p = Messages._collect_tags (self._msgs)
690         #reset _msgs as we iterated over it and can do so only once
691         self._msgs = None
692
693         if tags_p == None:
694             raise NotmuchError(STATUS.NULL_POINTER)
695         return Tags(tags_p, self)
696
697     def __iter__(self):
698         """ Make Messages an iterator """
699         return self
700
701     def next(self):
702         if self._msgs is None:
703             raise NotmuchError(STATUS.NOT_INITIALIZED)
704
705         if not nmlib.notmuch_messages_valid(self._msgs):
706             self._msgs = None
707             raise StopIteration
708
709         msg = Message(Messages._get (self._msgs), self)
710         nmlib.notmuch_messages_move_to_next(self._msgs)
711         return msg
712
713     def __len__(self):
714         """len(:class:`Messages`) returns the number of contained messages
715
716         .. note:: As this iterates over the messages, we will not be able to 
717                iterate over them again! So this will fail::
718
719                  #THIS FAILS
720                  msgs = Database().create_query('').search_message()
721                  if len(msgs) > 0:              #this 'exhausts' msgs
722                      # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
723                      for msg in msgs: print msg
724         """
725         if self._msgs is None:
726             raise NotmuchError(STATUS.NOT_INITIALIZED)
727
728         i=0
729         while nmlib.notmuch_messages_valid(self._msgs):
730             nmlib.notmuch_messages_move_to_next(self._msgs)
731             i += 1
732         self._msgs = None
733         return i
734
735
736
737     def __del__(self):
738         """Close and free the notmuch Messages"""
739         if self._msgs is not None:
740             logging.debug("Freeing the Messages now")
741             nmlib.notmuch_messages_destroy (self._msgs)
742
743
744 #------------------------------------------------------------------------------
745 class Message(object):
746     """Represents a single Email message
747
748     Technically, this wraps the underlying *notmuch_message_t* structure.
749     """
750
751     """notmuch_message_get_filename (notmuch_message_t *message)"""
752     _get_filename = nmlib.notmuch_message_get_filename
753     _get_filename.restype = c_char_p 
754
755     """notmuch_message_get_message_id (notmuch_message_t *message)"""
756     _get_message_id = nmlib.notmuch_message_get_message_id
757     _get_message_id.restype = c_char_p 
758
759     """notmuch_message_get_thread_id"""
760     _get_thread_id = nmlib.notmuch_message_get_thread_id
761     _get_thread_id.restype = c_char_p
762
763     """notmuch_message_get_replies"""
764     _get_replies = nmlib.notmuch_message_get_replies
765     _get_replies.restype = c_void_p
766
767     """notmuch_message_get_tags (notmuch_message_t *message)"""
768     _get_tags = nmlib.notmuch_message_get_tags
769     _get_tags.restype = c_void_p
770
771     _get_date = nmlib.notmuch_message_get_date
772     _get_date.restype = c_uint64
773
774     _get_header = nmlib.notmuch_message_get_header
775     _get_header.restype = c_char_p
776
777     def __init__(self, msg_p, parent=None):
778         """
779         :param msg_p: A pointer to an internal notmuch_message_t
780             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
781             STATUS.NULL_POINTER.
782         :param parent: A 'parent' object is passed which this message is
783               derived from. We save a reference to it, so we can
784               automatically delete the parent object once all derived
785               objects are dead.
786         """
787         if msg_p is None:
788             NotmuchError(STATUS.NULL_POINTER)
789         self._msg = msg_p
790         #keep reference to parent, so we keep it alive
791         self._parent = parent
792         logging.debug("Inited Message derived from %s" %(str(parent)))
793
794
795     def get_message_id(self):
796         """Returns the message ID
797         
798         :returns: String with a message ID
799         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
800                     is not initialized.
801         """
802         if self._msg is None:
803             raise NotmuchError(STATUS.NOT_INITIALIZED)
804         return Message._get_message_id(self._msg)
805
806     def get_thread_id(self):
807         """Returns the thread ID
808
809         The returned string belongs to 'message' will only be valid for as 
810         long as the message is valid.
811
812         This function will not return None since Notmuch ensures that every
813         message belongs to a single thread.
814
815         :returns: String with a thread ID
816         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
817                     is not initialized.
818         """
819         if self._msg is None:
820             raise NotmuchError(STATUS.NOT_INITIALIZED)
821
822         return Message._get_thread_id (self._msg);
823
824     def get_replies(self):
825         """Gets all direct replies to this message as :class:`Messages` iterator
826
827         .. note:: This call only makes sense if 'message' was
828           ultimately obtained from a :class:`Thread` object, (such as
829           by coming directly from the result of calling
830           :meth:`Thread.get_toplevel_messages` or by any number of
831           subsequent calls to :meth:`get_replies`). If this message was
832           obtained through some non-thread means, (such as by a call
833           to :meth:`Query.search_messages`), then this function will
834           return `None`.
835
836         :returns: :class:`Messages` or `None` if there are no replies to 
837             this message.
838         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
839                     is not initialized.
840         """
841         if self._msg is None:
842             raise NotmuchError(STATUS.NOT_INITIALIZED)
843
844         msgs_p = Message._get_replies(self._msg);
845
846         if msgs_p is None:
847             return None
848
849         return Messages(msgs_p,self)
850
851     def get_date(self):
852         """Returns time_t of the message date
853
854         For the original textual representation of the Date header from the
855         message call notmuch_message_get_header() with a header value of
856         "date".
857
858         :returns: a time_t timestamp
859         :rtype: c_unit64
860         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
861                     is not initialized.
862         """
863         if self._msg is None:
864             raise NotmuchError(STATUS.NOT_INITIALIZED)
865         return Message._get_date(self._msg)
866
867     def get_header(self, header):
868         """Returns a message header
869         
870         This returns any message header that is stored in the notmuch database.
871         This is only a selected subset of headers, which is currently:
872
873           TODO: add stored headers
874
875         :param header: The name of the header to be retrieved.
876                        It is not case-sensitive (TODO: confirm).
877         :type header: str
878         :returns: The header value as string
879         :exception: :exc:`NotmuchError`
880
881                     * STATUS.NOT_INITIALIZED if the message 
882                       is not initialized.
883                     * STATUS.NULL_POINTER, if no header was found
884         """
885         if self._msg is None:
886             raise NotmuchError(STATUS.NOT_INITIALIZED)
887
888         #Returns NULL if any error occurs.
889         header = Message._get_header (self._msg, header)
890         if header == None:
891             raise NotmuchError(STATUS.NULL_POINTER)
892         return header
893
894     def get_filename(self):
895         """Return the file path of the message file
896
897         :returns: Absolute file path & name of the message file
898         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
899               is not initialized.
900         """
901         if self._msg is None:
902             raise NotmuchError(STATUS.NOT_INITIALIZED)
903         return Message._get_filename(self._msg)
904
905     def get_tags(self):
906         """ Return the message tags
907
908         :returns: Message tags
909         :rtype: :class:`Tags`
910         :exception: :exc:`NotmuchError`
911
912                       * STATUS.NOT_INITIALIZED if the message 
913                         is not initialized.
914                       * STATUS.NULL_POINTER, on error
915         """
916         if self._msg is None:
917             raise NotmuchError(STATUS.NOT_INITIALIZED)
918
919         tags_p = Message._get_tags(self._msg)
920         if tags_p == None:
921             raise NotmuchError(STATUS.NULL_POINTER)
922         return Tags(tags_p, self)
923
924     def add_tag(self, tag):
925         """Add a tag to the given message
926
927         Adds a tag to the current message. The maximal tag length is defined in
928         the notmuch library and is currently 200 bytes.
929
930         :param tag: String with a 'tag' to be added.
931         :returns: STATUS.SUCCESS if the tag was successfully added.
932                   Raises an exception otherwise.
933         :exception: :exc:`NotmuchError`. They have the following meaning:
934
935                   STATUS.NULL_POINTER
936                     The 'tag' argument is NULL
937                   STATUS.TAG_TOO_LONG
938                     The length of 'tag' is too long 
939                     (exceeds Message.NOTMUCH_TAG_MAX)
940                   STATUS.READ_ONLY_DATABASE
941                     Database was opened in read-only mode so message cannot be 
942                     modified.
943                   STATUS.NOT_INITIALIZED
944                      The message has not been initialized.
945        """
946         if self._msg is None:
947             raise NotmuchError(STATUS.NOT_INITIALIZED)
948
949         status = nmlib.notmuch_message_add_tag (self._msg, tag)
950
951         if STATUS.SUCCESS == status:
952             # return on success
953             return status
954
955         raise NotmuchError(status)
956
957     def remove_tag(self, tag):
958         """Removes a tag from the given message
959
960         If the message has no such tag, this is a non-operation and
961         will report success anyway.
962
963         :param tag: String with a 'tag' to be removed.
964         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
965                   the message had no such tag.
966                   Raises an exception otherwise.
967         :exception: :exc:`NotmuchError`. They have the following meaning:
968
969                    STATUS.NULL_POINTER
970                      The 'tag' argument is NULL
971                    STATUS.TAG_TOO_LONG
972                      The length of 'tag' is too long
973                      (exceeds NOTMUCH_TAG_MAX)
974                    STATUS.READ_ONLY_DATABASE
975                      Database was opened in read-only mode so message cannot 
976                      be modified.
977                    STATUS.NOT_INITIALIZED
978                      The message has not been initialized.
979         """
980         if self._msg is None:
981             raise NotmuchError(STATUS.NOT_INITIALIZED)
982
983         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
984
985         if STATUS.SUCCESS == status:
986             # return on success
987             return status
988
989         raise NotmuchError(status)
990
991     def remove_all_tags(self):
992         """Removes all tags from the given message.
993
994         See :meth:`freeze` for an example showing how to safely
995         replace tag values.
996
997         :returns: STATUS.SUCCESS if the tags were successfully removed.
998                   Raises an exception otherwise.
999         :exception: :exc:`NotmuchError`. They have the following meaning:
1000
1001                    STATUS.READ_ONLY_DATABASE
1002                      Database was opened in read-only mode so message cannot 
1003                      be modified.
1004                    STATUS.NOT_INITIALIZED
1005                      The message has not been initialized.
1006         """
1007         if self._msg is None:
1008             raise NotmuchError(STATUS.NOT_INITIALIZED)
1009  
1010         status = nmlib.notmuch_message_remove_all_tags(self._msg)
1011
1012         if STATUS.SUCCESS == status:
1013             # return on success
1014             return status
1015
1016         raise NotmuchError(status)
1017
1018     def freeze(self):
1019         """Freezes the current state of 'message' within the database
1020
1021         This means that changes to the message state, (via :meth:`add_tag`, 
1022         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be 
1023         committed to the database until the message is :meth:`thaw`ed.
1024
1025         Multiple calls to freeze/thaw are valid and these calls will
1026         "stack". That is there must be as many calls to thaw as to freeze
1027         before a message is actually thawed.
1028
1029         The ability to do freeze/thaw allows for safe transactions to
1030         change tag values. For example, explicitly setting a message to
1031         have a given set of tags might look like this::
1032
1033           msg.freeze()
1034           msg.remove_all_tags()
1035           for tag in new_tags:
1036               msg.add_tag(tag)
1037           msg.thaw()
1038
1039         With freeze/thaw used like this, the message in the database is
1040         guaranteed to have either the full set of original tag values, or
1041         the full set of new tag values, but nothing in between.
1042
1043         Imagine the example above without freeze/thaw and the operation
1044         somehow getting interrupted. This could result in the message being
1045         left with no tags if the interruption happened after
1046         :meth:`remove_all_tags` but before :meth:`add_tag`.
1047
1048         :returns: STATUS.SUCCESS if the message was successfully frozen.
1049                   Raises an exception otherwise.
1050         :exception: :exc:`NotmuchError`. They have the following meaning:
1051
1052                    STATUS.READ_ONLY_DATABASE
1053                      Database was opened in read-only mode so message cannot 
1054                      be modified.
1055                    STATUS.NOT_INITIALIZED
1056                      The message has not been initialized.
1057         """
1058         if self._msg is None:
1059             raise NotmuchError(STATUS.NOT_INITIALIZED)
1060  
1061         status = nmlib.notmuch_message_freeze(self._msg)
1062
1063         if STATUS.SUCCESS == status:
1064             # return on success
1065             return status
1066
1067         raise NotmuchError(status)
1068
1069     def thaw(self):
1070         """Thaws the current 'message'
1071
1072         Thaw the current 'message', synchronizing any changes that may have 
1073         occurred while 'message' was frozen into the notmuch database.
1074
1075         See :meth:`freeze` for an example of how to use this
1076         function to safely provide tag changes.
1077
1078         Multiple calls to freeze/thaw are valid and these calls with
1079         "stack". That is there must be as many calls to thaw as to freeze
1080         before a message is actually thawed.
1081
1082         :returns: STATUS.SUCCESS if the message was successfully frozen.
1083                   Raises an exception otherwise.
1084         :exception: :exc:`NotmuchError`. They have the following meaning:
1085
1086                    STATUS.UNBALANCED_FREEZE_THAW
1087                      An attempt was made to thaw an unfrozen message. 
1088                      That is, there have been an unbalanced number of calls 
1089                      to :meth:`freeze` and :meth:`thaw`.
1090                    STATUS.NOT_INITIALIZED
1091                      The message has not been initialized.
1092         """
1093         if self._msg is None:
1094             raise NotmuchError(STATUS.NOT_INITIALIZED)
1095  
1096         status = nmlib.notmuch_message_thaw(self._msg)
1097
1098         if STATUS.SUCCESS == status:
1099             # return on success
1100             return status
1101
1102         raise NotmuchError(status)
1103
1104     
1105     def __str__(self):
1106         """A message() is represented by a 1-line summary"""
1107         msg = {}
1108         msg['from'] = self.get_header('from')
1109         msg['tags'] = str(self.get_tags())
1110         msg['date'] = date.fromtimestamp(self.get_date())
1111         replies = self.get_replies()
1112         msg['replies'] = len(replies) if replies is not None else -1
1113         return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg)
1114
1115     def format_as_text(self):
1116         """Output like notmuch show (Not implemented)"""
1117         return str(self)
1118
1119     def __del__(self):
1120         """Close and free the notmuch Message"""
1121         if self._msg is not None:
1122             logging.debug("Freeing the Message now")
1123             nmlib.notmuch_message_destroy (self._msg)