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