]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/database.py
Add Database.create_query() as a shorthand for db=Database();q=Query(db,"")
[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           # short version
301           # Automatically frees the Database() when 'q' is deleted
302
303           q  = Database(dbpath).create_query('from:"Biene Maja"')
304
305           # long version, which is functionally equivalent but will keep the
306           # Database in the 'db' variable around after we delete 'q':
307
308           db = Database(dbpath)
309           q  = Query(db,'from:"Biene Maja"')
310
311         This function is a python extension and not in the underlying C API.
312         """
313         # Raise a NotmuchError if not initialized
314         self._verify_initialized_db()
315
316         return Query(self._db, querystring)
317
318     def __repr__(self):
319         return "'Notmuch DB " + self.get_path() + "'"
320
321     def __del__(self):
322         """Close and free the notmuch database if needed"""
323         if self._db is not None:
324             logging.debug("Freeing the database now")
325             nmlib.notmuch_database_close(self._db)
326
327     def _get_user_default_db(self):
328         """ Reads a user's notmuch config and returns his db location
329
330         Throws a NotmuchError if it cannot find it"""
331         from ConfigParser import SafeConfigParser
332         config = SafeConfigParser()
333         conf_f = os.getenv('NOTMUCH_CONFIG',
334                            os.path.expanduser('~/.notmuch-config'))
335         config.read(conf_f)
336         if not config.has_option('database','path'):
337             raise NotmuchError(message=
338                                "No DB path specified and no user default found")
339         return config.get('database','path')
340
341     @property
342     def db_p(self):
343         """Property returning a pointer to `notmuch_database_t` or `None`
344
345         This should normally not be needed by a user (and is not yet
346         guaranteed to remain stable in future versions).
347         """
348         return self._db
349
350 #------------------------------------------------------------------------------
351 class Query(object):
352     """Represents a search query on an opened :class:`Database`.
353
354     A query selects and filters a subset of messages from the notmuch
355     database we derive from.
356
357     Technically, it wraps the underlying *notmuch_query_t* struct.
358
359     .. note:: Do remember that as soon as we tear down this object,
360            all underlying derived objects such as threads,
361            messages, tags etc will be freed by the underlying library
362            as well. Accessing these objects will lead to segfaults and
363            other unexpected behavior. See above for more details.
364     """
365     # constants
366     SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
367     """Constants: Sort order in which to return results"""
368
369     """notmuch_query_create"""
370     _create = nmlib.notmuch_query_create
371     _create.restype = c_void_p
372
373     """notmuch_query_search_messages"""
374     _search_messages = nmlib.notmuch_query_search_messages
375     _search_messages.restype = c_void_p
376
377
378     """notmuch_query_count_messages"""
379     _count_messages = nmlib.notmuch_query_count_messages
380     _count_messages.restype = c_uint
381
382     def __init__(self, db, querystr):
383         """
384         :param db: An open database which we derive the Query from.
385         :type db: :class:`Database`
386         :param querystr: The query string for the message.
387         :type querystr: str
388         """
389         self._db = None
390         self._query = None
391         self.create(db, querystr)
392
393     def create(self, db, querystr):
394         """Creates a new query derived from a Database
395
396         This function is utilized by __init__() and usually does not need to 
397         be called directly.
398
399         :param db: Database to create the query from.
400         :type db: :class:`Database`
401         :param querystr: The query string
402         :type querystr: str
403         :returns: Nothing
404         :exception: :exc:`NotmuchError`
405
406                       * STATUS.NOT_INITIALIZED if db is not inited
407                       * STATUS.NULL_POINTER if the query creation failed 
408                         (too little memory)
409         """
410         if db.db_p is None:
411             raise NotmuchError(STATUS.NOT_INITIALIZED)            
412         # create reference to parent db to keep it alive
413         self._db = db
414         
415         # create query, return None if too little mem available
416         query_p = Query._create(db.db_p, querystr)
417         if query_p is None:
418             NotmuchError(STATUS.NULL_POINTER)
419         self._query = query_p
420
421     def set_sort(self, sort):
422         """Set the sort order future results will be delivered in
423
424         Wraps the underlying *notmuch_query_set_sort* function.
425
426         :param sort: Sort order (see :attr:`Query.SORT`)
427         :returns: Nothing
428         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
429                     been initialized.
430         """
431         if self._query is None:
432             raise NotmuchError(STATUS.NOT_INITIALIZED)
433
434         nmlib.notmuch_query_set_sort(self._query, sort)
435
436     def search_messages(self):
437         """Filter messages according to the query and return
438         :class:`Messages` in the defined sort order
439
440         Technically, it wraps the underlying
441         *notmuch_query_search_messages* function.
442
443         :returns: :class:`Messages`
444         :exception: :exc:`NotmuchError`
445
446                       * STATUS.NOT_INITIALIZED if query is not inited
447                       * STATUS.NULL_POINTER if search_messages failed 
448         """
449         if self._query is None:
450             raise NotmuchError(STATUS.NOT_INITIALIZED)            
451
452         msgs_p = Query._search_messages(self._query)
453
454         if msgs_p is None:
455             NotmuchError(STATUS.NULL_POINTER)
456
457         return Messages(msgs_p,self)
458
459     def count_messages(self):
460         """Estimate the number of messages matching the query
461
462         This function performs a search and returns Xapian's best
463         guess as to the number of matching messages. It is much faster
464         than performing :meth:`search_messages` and counting the
465         result with `len()` (although it always returned the same
466         result in my tests). Technically, it wraps the underlying
467         *notmuch_query_count_messages* function.
468
469         :returns: :class:`Messages`
470         :exception: :exc:`NotmuchError`
471
472                       * STATUS.NOT_INITIALIZED if query is not inited
473         """
474         if self._query is None:
475             raise NotmuchError(STATUS.NOT_INITIALIZED)            
476
477         return Query._count_messages(self._query)
478
479     def __del__(self):
480         """Close and free the Query"""
481         if self._query is not None:
482             logging.debug("Freeing the Query now")
483             nmlib.notmuch_query_destroy (self._query)
484
485 #------------------------------------------------------------------------------
486 class Tags(object):
487     """Represents a list of notmuch tags
488
489     This object provides an iterator over a list of notmuch tags. Do
490     note that the underlying library only provides a one-time iterator
491     (it cannot reset the iterator to the start). Thus iterating over
492     the function will "exhaust" the list of tags, and a subsequent
493     iteration attempt will raise a :exc:`NotmuchError`
494     STATUS.NOT_INITIALIZED. Also note, that any function that uses
495     iteration (nearly all) will also exhaust the tags. So both::
496
497       for tag in tags: print tag 
498
499     as well as::
500
501        number_of_tags = len(tags)
502
503     and even a simple::
504
505        #str() iterates over all tags to construct a space separated list
506        print(str(tags))
507
508     will "exhaust" the Tags. If you need to re-iterate over a list of
509     tags you will need to retrieve a new :class:`Tags` object.
510     """
511
512     #notmuch_tags_get
513     _get = nmlib.notmuch_tags_get
514     _get.restype = c_char_p
515
516     def __init__(self, tags_p, parent=None):
517         """
518         :param tags_p: A pointer to an underlying *notmuch_tags_t*
519              structure. These are not publically exposed, so a user
520              will almost never instantiate a :class:`Tags` object
521              herself. They are usually handed back as a result,
522              e.g. in :meth:`Database.get_all_tags`.  *tags_p* must be
523              valid, we will raise an :exc:`NotmuchError`
524              (STATUS.NULL_POINTER) if it is `None`.
525         :type tags_p: :class:`ctypes.c_void_p`
526         :param parent: The parent object (ie :class:`Database` or 
527              :class:`Message` these tags are derived from, and saves a
528              reference to it, so we can automatically delete the db object
529              once all derived objects are dead.
530         :TODO: Make the iterator optionally work more than once by
531                cache the tags in the Python object(?)
532         """
533         if tags_p is None:
534             NotmuchError(STATUS.NULL_POINTER)
535
536         self._tags = tags_p
537         #save reference to parent object so we keep it alive
538         self._parent = parent
539         logging.debug("Inited Tags derived from %s" %(repr(parent)))
540     
541     def __iter__(self):
542         """ Make Tags an iterator """
543         return self
544
545     def next(self):
546         if self._tags is None:
547             raise NotmuchError(STATUS.NOT_INITIALIZED)
548
549         if not nmlib.notmuch_tags_valid(self._tags):
550             self._tags = None
551             raise StopIteration
552
553         tag = Tags._get (self._tags)
554         nmlib.notmuch_tags_move_to_next(self._tags)
555         return tag
556
557     def __len__(self):
558         """len(:class:`Tags`) returns the number of contained tags
559
560         .. note:: As this iterates over the tags, we will not be able
561                to iterate over them again (as in retrieve them)! If
562                the tags have been exhausted already, this will raise a
563                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
564                subsequent attempts.
565         """
566         if self._tags is None:
567             raise NotmuchError(STATUS.NOT_INITIALIZED)
568
569         i=0
570         while nmlib.notmuch_tags_valid(self._msgs):
571             nmlib.notmuch_tags_move_to_next(self._msgs)
572             i += 1
573         self._tags = None
574         return i
575
576     def __str__(self):
577         """The str() representation of Tags() is a space separated list of tags
578
579         .. note:: As this iterates over the tags, we will not be able
580                to iterate over them again (as in retrieve them)! If
581                the tags have been exhausted already, this will raise a
582                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
583                subsequent attempts.
584         """
585         return " ".join(self)
586
587     def __del__(self):
588         """Close and free the notmuch tags"""
589         if self._tags is not None:
590             logging.debug("Freeing the Tags now")
591             nmlib.notmuch_tags_destroy (self._tags)
592
593
594 #------------------------------------------------------------------------------
595 class Messages(object):
596     """Represents a list of notmuch messages
597
598     This object provides an iterator over a list of notmuch messages
599     (Technically, it provides a wrapper for the underlying
600     *notmuch_messages_t* structure). Do note that the underlying
601     library only provides a one-time iterator (it cannot reset the
602     iterator to the start). Thus iterating over the function will
603     "exhaust" the list of messages, and a subsequent iteration attempt
604     will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
605     note, that any function that uses iteration will also
606     exhaust the messages. So both::
607
608       for msg in msgs: print msg 
609
610     as well as::
611
612        number_of_msgs = len(msgs)
613
614     will "exhaust" the Messages. If you need to re-iterate over a list of
615     messages you will need to retrieve a new :class:`Messages` object.
616
617     Things are not as bad as it seems though, you can store and reuse
618     the single Message objects as often as you want as long as you
619     keep the parent Messages object around. (Recall that due to
620     hierarchical memory allocation, all derived Message objects will
621     be invalid when we delete the parent Messages() object, even if it
622     was already "exhausted".) So this works::
623
624       db   = Database()
625       msgs = Query(db,'').search_messages() #get a Messages() object
626       msglist = []
627       for m in msgs:
628          msglist.append(m)
629
630       # msgs is "exhausted" now and even len(msgs) will raise an exception.
631       # However it will be kept around until all retrieved Message() objects are
632       # also deleted. If you did e.g. an explicit del(msgs) here, the 
633       # following lines would fail.
634       
635       # You can reiterate over *msglist* however as often as you want. 
636       # It is simply a list with Message objects.
637
638       print (msglist[0].get_filename())
639       print (msglist[1].get_filename())
640       print (msglist[0].get_message_id())
641     """
642
643     #notmuch_tags_get
644     _get = nmlib.notmuch_messages_get
645     _get.restype = c_void_p
646
647     _collect_tags = nmlib.notmuch_messages_collect_tags
648     _collect_tags.restype = c_void_p
649
650     def __init__(self, msgs_p, parent=None):
651         """
652         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
653              structure. These are not publically exposed, so a user
654              will almost never instantiate a :class:`Messages` object
655              herself. They are usually handed back as a result,
656              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
657              valid, we will raise an :exc:`NotmuchError`
658              (STATUS.NULL_POINTER) if it is `None`.
659         :type msgs_p: :class:`ctypes.c_void_p`
660         :param parent: The parent object
661              (ie :class:`Query`) these tags are derived from. It saves
662              a reference to it, so we can automatically delete the db
663              object once all derived objects are dead.
664         :TODO: Make the iterator work more than once and cache the tags in 
665                the Python object.(?)
666         """
667         if msgs_p is None:
668             NotmuchError(STATUS.NULL_POINTER)
669
670         self._msgs = msgs_p
671         #store parent, so we keep them alive as long as self  is alive
672         self._parent = parent
673         logging.debug("Inited Messages derived from %s" %(str(parent)))
674
675     def collect_tags(self):
676         """Return the unique :class:`Tags` in the contained messages
677
678         :returns: :class:`Tags`
679         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
680
681         .. note:: :meth:`collect_tags` will iterate over the messages and
682           therefore will not allow further iterations.
683         """
684         if self._msgs is None:
685             raise NotmuchError(STATUS.NOT_INITIALIZED)
686
687         # collect all tags (returns NULL on error)
688         tags_p = Messages._collect_tags (self._msgs)
689         #reset _msgs as we iterated over it and can do so only once
690         self._msgs = None
691
692         if tags_p == None:
693             raise NotmuchError(STATUS.NULL_POINTER)
694         return Tags(tags_p, self)
695
696     def __iter__(self):
697         """ Make Messages an iterator """
698         return self
699
700     def next(self):
701         if self._msgs is None:
702             raise NotmuchError(STATUS.NOT_INITIALIZED)
703
704         if not nmlib.notmuch_messages_valid(self._msgs):
705             self._msgs = None
706             raise StopIteration
707
708         msg = Message(Messages._get (self._msgs), self)
709         nmlib.notmuch_messages_move_to_next(self._msgs)
710         return msg
711
712     def __len__(self):
713         """len(:class:`Messages`) returns the number of contained messages
714
715         .. note:: As this iterates over the messages, we will not be able to 
716                iterate over them again (as in retrieve them)!
717         """
718         if self._msgs is None:
719             raise NotmuchError(STATUS.NOT_INITIALIZED)
720
721         i=0
722         while nmlib.notmuch_messages_valid(self._msgs):
723             nmlib.notmuch_messages_move_to_next(self._msgs)
724             i += 1
725         self._msgs = None
726         return i
727
728
729
730     def __del__(self):
731         """Close and free the notmuch Messages"""
732         if self._msgs is not None:
733             logging.debug("Freeing the Messages now")
734             nmlib.notmuch_messages_destroy (self._msgs)
735
736
737 #------------------------------------------------------------------------------
738 class Message(object):
739     """Represents a single Email message
740
741     Technically, this wraps the underlying *notmuch_message_t* structure.
742     """
743
744     """notmuch_message_get_filename (notmuch_message_t *message)"""
745     _get_filename = nmlib.notmuch_message_get_filename
746     _get_filename.restype = c_char_p 
747
748     """notmuch_message_get_message_id (notmuch_message_t *message)"""
749     _get_message_id = nmlib.notmuch_message_get_message_id
750     _get_message_id.restype = c_char_p 
751
752     """notmuch_message_get_thread_id"""
753     _get_thread_id = nmlib.notmuch_message_get_thread_id
754     _get_thread_id.restype = c_char_p
755
756     """notmuch_message_get_tags (notmuch_message_t *message)"""
757     _get_tags = nmlib.notmuch_message_get_tags
758     _get_tags.restype = c_void_p
759
760     _get_date = nmlib.notmuch_message_get_date
761     _get_date.restype = c_uint64
762
763     _get_header = nmlib.notmuch_message_get_header
764     _get_header.restype = c_char_p
765
766     def __init__(self, msg_p, parent=None):
767         """
768         :param msg_p: A pointer to an internal notmuch_message_t
769             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
770             STATUS.NULL_POINTER.
771         :param parent: A 'parent' object is passed which this message is
772               derived from. We save a reference to it, so we can
773               automatically delete the parent object once all derived
774               objects are dead.
775         """
776         if msg_p is None:
777             NotmuchError(STATUS.NULL_POINTER)
778         self._msg = msg_p
779         #keep reference to parent, so we keep it alive
780         self._parent = parent
781         logging.debug("Inited Message derived from %s" %(str(parent)))
782
783
784     def get_message_id(self):
785         """Returns the message ID
786         
787         :returns: String with a message ID
788         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
789                     is not initialized.
790         """
791         if self._msg is None:
792             raise NotmuchError(STATUS.NOT_INITIALIZED)
793         return Message._get_message_id(self._msg)
794
795     def get_thread_id(self):
796         """Returns the thread ID
797
798         The returned string belongs to 'message' will only be valid for as 
799         long as the message is valid.
800
801         This function will not return None since Notmuch ensures that every
802         message belongs to a single thread.
803
804         :returns: String with a thread ID
805         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
806                     is not initialized.
807         """
808         if self._msg is None:
809             raise NotmuchError(STATUS.NOT_INITIALIZED)
810
811         return Message._get_thread_id (self._msg);
812
813     def get_date(self):
814         """Returns time_t of the message date
815
816         For the original textual representation of the Date header from the
817         message call notmuch_message_get_header() with a header value of
818         "date".
819
820         :returns: a time_t timestamp
821         :rtype: c_unit64
822         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
823                     is not initialized.
824         """
825         if self._msg is None:
826             raise NotmuchError(STATUS.NOT_INITIALIZED)
827         return Message._get_date(self._msg)
828
829     def get_header(self, header):
830         """Returns a message header
831         
832         This returns any message header that is stored in the notmuch database.
833         This is only a selected subset of headers, which is currently:
834
835           TODO: add stored headers
836
837         :param header: The name of the header to be retrieved.
838                        It is not case-sensitive (TODO: confirm).
839         :type header: str
840         :returns: The header value as string
841         :exception: :exc:`NotmuchError`
842
843                     * STATUS.NOT_INITIALIZED if the message 
844                       is not initialized.
845                     * STATUS.NULL_POINTER, if no header was found
846         """
847         if self._msg is None:
848             raise NotmuchError(STATUS.NOT_INITIALIZED)
849
850         #Returns NULL if any error occurs.
851         header = Message._get_header (self._msg, header)
852         if header == None:
853             raise NotmuchError(STATUS.NULL_POINTER)
854         return header
855
856     def get_filename(self):
857         """Return the file path of the message file
858
859         :returns: Absolute file path & name of the message file
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_filename(self._msg)
866
867     def get_tags(self):
868         """ Return the message tags
869
870         :returns: Message tags
871         :rtype: :class:`Tags`
872         :exception: :exc:`NotmuchError`
873
874                       * STATUS.NOT_INITIALIZED if the message 
875                         is not initialized.
876                       * STATUS.NULL_POINTER, on error
877         """
878         if self._msg is None:
879             raise NotmuchError(STATUS.NOT_INITIALIZED)
880
881         tags_p = Message._get_tags(self._msg)
882         if tags_p == None:
883             raise NotmuchError(STATUS.NULL_POINTER)
884         return Tags(tags_p, self)
885
886     def add_tag(self, tag):
887         """Add a tag to the given message
888
889         Adds a tag to the current message. The maximal tag length is defined in
890         the notmuch library and is currently 200 bytes.
891
892         :param tag: String with a 'tag' to be added.
893         :returns: STATUS.SUCCESS if the tag was successfully added.
894                   Raises an exception otherwise.
895         :exception: :exc:`NotmuchError`. They have the following meaning:
896
897                   STATUS.NULL_POINTER
898                     The 'tag' argument is NULL
899                   STATUS.TAG_TOO_LONG
900                     The length of 'tag' is too long 
901                     (exceeds Message.NOTMUCH_TAG_MAX)
902                   STATUS.READ_ONLY_DATABASE
903                     Database was opened in read-only mode so message cannot be 
904                     modified.
905                   STATUS.NOT_INITIALIZED
906                      The message has not been initialized.
907        """
908         if self._msg is None:
909             raise NotmuchError(STATUS.NOT_INITIALIZED)
910
911         status = nmlib.notmuch_message_add_tag (self._msg, tag)
912
913         if STATUS.SUCCESS == status:
914             # return on success
915             return status
916
917         raise NotmuchError(status)
918
919     def remove_tag(self, tag):
920         """Removes a tag from the given message
921
922         If the message has no such tag, this is a non-operation and
923         will report success anyway.
924
925         :param tag: String with a 'tag' to be removed.
926         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
927                   the message had no such tag.
928                   Raises an exception otherwise.
929         :exception: :exc:`NotmuchError`. They have the following meaning:
930
931                    STATUS.NULL_POINTER
932                      The 'tag' argument is NULL
933                    STATUS.TAG_TOO_LONG
934                      The length of 'tag' is too long
935                      (exceeds NOTMUCH_TAG_MAX)
936                    STATUS.READ_ONLY_DATABASE
937                      Database was opened in read-only mode so message cannot 
938                      be modified.
939                    STATUS.NOT_INITIALIZED
940                      The message has not been initialized.
941         """
942         if self._msg is None:
943             raise NotmuchError(STATUS.NOT_INITIALIZED)
944
945         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
946
947         if STATUS.SUCCESS == status:
948             # return on success
949             return status
950
951         raise NotmuchError(status)
952
953     def remove_all_tags(self):
954         """Removes all tags from the given message.
955
956         See :meth:`freeze` for an example showing how to safely
957         replace tag values.
958
959         :returns: STATUS.SUCCESS if the tags were successfully removed.
960                   Raises an exception otherwise.
961         :exception: :exc:`NotmuchError`. They have the following meaning:
962
963                    STATUS.READ_ONLY_DATABASE
964                      Database was opened in read-only mode so message cannot 
965                      be modified.
966                    STATUS.NOT_INITIALIZED
967                      The message has not been initialized.
968         """
969         if self._msg is None:
970             raise NotmuchError(STATUS.NOT_INITIALIZED)
971  
972         status = nmlib.notmuch_message_remove_all_tags(self._msg)
973
974         if STATUS.SUCCESS == status:
975             # return on success
976             return status
977
978         raise NotmuchError(status)
979
980     def freeze(self):
981         """Freezes the current state of 'message' within the database
982
983         This means that changes to the message state, (via :meth:`add_tag`, 
984         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be 
985         committed to the database until the message is :meth:`thaw`ed.
986
987         Multiple calls to freeze/thaw are valid and these calls will
988         "stack". That is there must be as many calls to thaw as to freeze
989         before a message is actually thawed.
990
991         The ability to do freeze/thaw allows for safe transactions to
992         change tag values. For example, explicitly setting a message to
993         have a given set of tags might look like this::
994
995           msg.freeze()
996           msg.remove_all_tags()
997           for tag in new_tags:
998               msg.add_tag(tag)
999           msg.thaw()
1000
1001         With freeze/thaw used like this, the message in the database is
1002         guaranteed to have either the full set of original tag values, or
1003         the full set of new tag values, but nothing in between.
1004
1005         Imagine the example above without freeze/thaw and the operation
1006         somehow getting interrupted. This could result in the message being
1007         left with no tags if the interruption happened after
1008         :meth:`remove_all_tags` but before :meth:`add_tag`.
1009
1010         :returns: STATUS.SUCCESS if the message was successfully frozen.
1011                   Raises an exception otherwise.
1012         :exception: :exc:`NotmuchError`. They have the following meaning:
1013
1014                    STATUS.READ_ONLY_DATABASE
1015                      Database was opened in read-only mode so message cannot 
1016                      be modified.
1017                    STATUS.NOT_INITIALIZED
1018                      The message has not been initialized.
1019         """
1020         if self._msg is None:
1021             raise NotmuchError(STATUS.NOT_INITIALIZED)
1022  
1023         status = nmlib.notmuch_message_freeze(self._msg)
1024
1025         if STATUS.SUCCESS == status:
1026             # return on success
1027             return status
1028
1029         raise NotmuchError(status)
1030
1031     def thaw(self):
1032         """Thaws the current 'message'
1033
1034         Thaw the current 'message', synchronizing any changes that may have 
1035         occurred while 'message' was frozen into the notmuch database.
1036
1037         See :meth:`freeze` for an example of how to use this
1038         function to safely provide tag changes.
1039
1040         Multiple calls to freeze/thaw are valid and these calls with
1041         "stack". That is there must be as many calls to thaw as to freeze
1042         before a message is actually thawed.
1043
1044         :returns: STATUS.SUCCESS if the message was successfully frozen.
1045                   Raises an exception otherwise.
1046         :exception: :exc:`NotmuchError`. They have the following meaning:
1047
1048                    STATUS.UNBALANCED_FREEZE_THAW
1049                      An attempt was made to thaw an unfrozen message. 
1050                      That is, there have been an unbalanced number of calls 
1051                      to :meth:`freeze` and :meth:`thaw`.
1052                    STATUS.NOT_INITIALIZED
1053                      The message has not been initialized.
1054         """
1055         if self._msg is None:
1056             raise NotmuchError(STATUS.NOT_INITIALIZED)
1057  
1058         status = nmlib.notmuch_message_thaw(self._msg)
1059
1060         if STATUS.SUCCESS == status:
1061             # return on success
1062             return status
1063
1064         raise NotmuchError(status)
1065
1066     
1067     def __str__(self):
1068         """A message() is represented by a 1-line summary"""
1069         msg = {}
1070         msg['from'] = self.get_header('from')
1071         msg['tags'] = str(self.get_tags())
1072         msg['date'] = date.fromtimestamp(self.get_date())
1073         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
1074
1075     def format_as_text(self):
1076         """Output like notmuch show (Not implemented)"""
1077         return str(self)
1078
1079     def __del__(self):
1080         """Close and free the notmuch Message"""
1081         if self._msg is not None:
1082             logging.debug("Freeing the Message now")
1083             nmlib.notmuch_message_destroy (self._msg)