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