ad84f5eb2048628b20a194d8539e0eae6bbe1ef8
[notmuch] / cnotmuch / database.py
1 import ctypes
2 from ctypes import c_int, c_char_p, c_void_p, c_uint, c_uint64, c_bool
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     MODE = Enum(['READ_ONLY','READ_WRITE'])
17     """Constants: Mode in which to open the database"""
18
19     _std_db_path = None
20     """Class attribute to cache user's default 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= MODE.READ_ONLY):
47         """If *path* is *None*, we will try to read a users notmuch
48         configuration and use his default database. If *create* is `True`,
49         the database will always be created in
50         :attr:`MODE`.READ_WRITE mode.
51
52         :param path:   Directory to open/create the database in (see
53                        above for behavior if `None`)
54         :type path:    `str` or `None`
55         :param create: Pass `False` to open an existing, `True` to create a new
56                        database.  
57         :type create:  bool
58         :param mode:   Mode to open a database in. Is always 
59                        :attr:`MODE`.READ_WRITE when creating a new one.
60         :type mode:    :attr:`MODE`
61         :returns:      Nothing
62         :exception:    :exc:`NotmuchError` in case of failure.
63         """
64         self._db = None
65         if path is None:
66             # no path specified. use a user's default database
67             if Database._std_db_path is None:
68                 #the following line throws a NotmuchError if it fails
69                 Database._std_db_path = self._get_user_default_db()
70             path = Database._std_db_path
71
72         if create == False:
73             self.open(path, mode)
74         else:
75             self.create(path)
76
77     def create(self, path):
78         """Creates a new notmuch database
79
80         This function is used by __init__() and usually does not need
81         to be called directly. It wraps the underlying
82         *notmuch_database_create* function and creates a new notmuch
83         database at *path*. It will always return a database in
84         :attr:`MODE`.READ_WRITE mode as creating an empty database for
85         reading only does not make a great deal of sense.
86
87         :param path: A directory in which we should create the database.
88         :type path: str
89         :returns: Nothing
90         :exception: :exc:`NotmuchError` in case of any failure
91                     (after printing an error message on stderr).
92         """
93         if self._db is not None:
94             raise NotmuchError(
95             message="Cannot create db, this Database() already has an open one.")
96
97         res = Database._create(path, MODE.READ_WRITE)
98
99         if res is None:
100             raise NotmuchError(
101                 message="Could not create the specified database")
102         self._db = res
103
104     def open(self, path, mode= MODE.READ_ONLY): 
105         """Opens an existing database
106
107         This function is used by __init__() and usually does not need
108         to be called directly. It wraps the underlying
109         *notmuch_database_open* function.
110
111         :param status: Open the database in read-only or read-write mode
112         :type status:  :attr:`MODE` 
113         :returns: Nothing
114         :exception: Raises :exc:`NotmuchError` in case
115                     of any failure (after printing an error message on stderr).
116         """
117
118         res = Database._open(path, mode)
119
120         if res is None:
121             raise NotmuchError(
122                 message="Could not open the specified database")
123         self._db = res
124
125     def get_path(self):
126         """Returns the file path of an open database
127
128         Wraps notmuch_database_get_path"""
129         return Database._get_path(self._db)
130
131     def get_version(self):
132         """Returns the database format version
133
134         :returns: The database version as positive integer
135         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
136                     the database was not intitialized.
137         """
138         if self._db is None:
139             raise NotmuchError(STATUS.NOT_INITIALIZED)
140
141         return Database._get_version (self._db)
142
143     def needs_upgrade(self):
144         """Does this database need to be upgraded before writing to it?
145
146         If this function returns TRUE then no functions that modify the
147         database (:meth:`Database.add_message`, :meth:`Database.add_tag`,
148         :meth:`Directory.set_mtime`, etc.) will work unless :meth:`upgrade` 
149         is called successfully first.
150
151         :returns: `True` or `False`
152         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
153                     the database was not intitialized.
154         """
155         if self._db is None:
156             raise NotmuchError(STATUS.NOT_INITIALIZED)
157
158         return notmuch_database_needs_upgrade(self.db) 
159
160     def find_message(self, msgid):
161         """Returns a :class:`Message` as identified by its message ID
162
163         Wraps the underlying *notmuch_database_find_message* function.
164
165         :param msgid: The message ID
166         :type msgid: string
167         :returns: :class:`Message` or `None` if no message is found or if an
168                   out-of-memory situation occurs.
169         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
170                   the database was not intitialized.
171         """
172         if self._db is None:
173             raise NotmuchError(STATUS.NOT_INITIALIZED)
174         msg_p = Database._find_message(self._db, msgid)
175         if msg_p is None:
176             return None
177         return Message(msg_p, self)
178
179     def get_all_tags(self):
180         """Returns :class:`Tags` with a list of all tags found in the database
181
182         :returns: :class:`Tags`
183         :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
184         """
185         if self._db is None:
186             raise NotmuchError(STATUS.NOT_INITIALIZED)
187
188         tags_p = Database._get_all_tags (self._db)
189         if tags_p == None:
190             raise NotmuchError(STATUS.NULL_POINTER)
191         return Tags(tags_p, self)
192
193     def __repr__(self):
194         return "'Notmuch DB " + self.get_path() + "'"
195
196     def __del__(self):
197         """Close and free the notmuch database if needed"""
198         if self._db is not None:
199             logging.debug("Freeing the database now")
200             nmlib.notmuch_database_close(self._db)
201
202     def _get_user_default_db(self):
203         """ Reads a user's notmuch config and returns his db location
204
205         Throws a NotmuchError if it cannot find it"""
206         from ConfigParser import SafeConfigParser
207         import os.path
208         config = SafeConfigParser()
209         config.read(os.path.expanduser('~/.notmuch-config'))
210         if not config.has_option('database','path'):
211             raise NotmuchError(message=
212                                "No DB path specified and no user default found")
213         return config.get('database','path')
214
215     @property
216     def db_p(self):
217         """Property returning a pointer to the notmuch_database_t or `None`
218
219         This should normally not be needed by a user."""
220         return self._db
221
222 #------------------------------------------------------------------------------
223 class Query(object):
224     """ Represents a search query on an opened :class:`Database`.
225
226     A query selects and filters a subset of messages from the notmuch
227     database we derive from.
228
229     Technically, it wraps the underlying *notmuch_query_t* struct.
230
231     .. note:: Do remember that as soon as we tear down this object,
232            all underlying derived objects such as threads,
233            messages, tags etc will be freed by the underlying library
234            as well. Accessing these objects will lead to segfaults and
235            other unexpected behavior. See above for more details.
236     """
237     # constants
238     SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
239     """Constants: Sort order in which to return results"""
240
241     """notmuch_query_create"""
242     _create = nmlib.notmuch_query_create
243     _create.restype = c_void_p
244
245     """notmuch_query_search_messages"""
246     _search_messages = nmlib.notmuch_query_search_messages
247     _search_messages.restype = c_void_p
248
249     def __init__(self, db, querystr):
250         """
251         :param db: An open database which we derive the Query from.
252         :type db: :class:`Database`
253         :param querystr: The query string for the message.
254         :type querystr: str
255         """
256         self._db = None
257         self._query = None
258         self.create(db, querystr)
259
260     def create(self, db, querystr):
261         """Creates a new query derived from a Database.
262
263         This function is utilized by __init__() and usually does not need to 
264         be called directly.
265
266         :param db: Database to create the query from.
267         :type db: :class:`Database`
268         :param querystr: The query string
269         :type querystr: str
270         :returns: Nothing
271         :exception: :exc:`NotmuchError`
272
273                       * STATUS.NOT_INITIALIZED if db is not inited
274                       * STATUS.NULL_POINTER if the query creation failed 
275                         (too little memory)
276         """
277         if db.db_p is None:
278             raise NotmuchError(STATUS.NOT_INITIALIZED)            
279         # create reference to parent db to keep it alive
280         self._db = db
281         
282         # create query, return None if too little mem available
283         query_p = Query._create(db.db_p, querystr)
284         if query_p is None:
285             NotmuchError(STATUS.NULL_POINTER)
286         self._query = query_p
287
288     def set_sort(self, sort):
289         """Set the sort order future results will be delivered in
290
291         Wraps the underlying *notmuch_query_set_sort* function.
292
293         :param sort: Sort order (see :attr:`Query.SORT`)
294         :returns: Nothing
295         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not 
296                     been initialized.
297         """
298         if self._query is None:
299             raise NotmuchError(STATUS.NOT_INITIALIZED)
300
301         nmlib.notmuch_query_set_sort(self._query, sort)
302
303     def search_messages(self):
304         """Filter messages according to the query and return
305         :class:`Messages` in the defined sort order
306
307         Technically, it wraps the underlying
308         *notmuch_query_search_messages* function.
309
310         :returns: :class:`Messages`
311         :exception: :exc:`NotmuchError`
312
313                       * STATUS.NOT_INITIALIZED if query is not inited
314                       * STATUS.NULL_POINTER if search_messages failed 
315         """
316         if self._query is None:
317             raise NotmuchError(STATUS.NOT_INITIALIZED)            
318
319         msgs_p = Query._search_messages(self._query)
320
321         if msgs_p is None:
322             NotmuchError(STATUS.NULL_POINTER)
323
324         return Messages(msgs_p,self)
325
326
327     def __del__(self):
328         """Close and free the Query"""
329         if self._query is not None:
330             logging.debug("Freeing the Query now")
331             nmlib.notmuch_query_destroy (self._query)
332
333 #------------------------------------------------------------------------------
334 class Tags(object):
335     """Represents a list of notmuch tags
336
337     This object provides an iterator over a list of notmuch tags. Do
338     note that the underlying library only provides a one-time iterator
339     (it cannot reset the iterator to the start). Thus iterating over
340     the function will "exhaust" the list of tags, and a subsequent
341     iteration attempt will raise a :exc:`NotmuchError`
342     STATUS.NOT_INITIALIZED. Also note, that any function that uses
343     iteration (nearly all) will also exhaust the tags. So both::
344
345       for tag in tags: print tag 
346
347     as well as::
348
349        number_of_tags = len(tags)
350
351     and even a simple::
352
353        #str() iterates over all tags to construct a space separated list
354        print(str(tags))
355
356     will "exhaust" the Tags. If you need to re-iterate over a list of
357     tags you will need to retrieve a new :class:`Tags` object.
358     """
359
360     #notmuch_tags_get
361     _get = nmlib.notmuch_tags_get
362     _get.restype = c_char_p
363
364     def __init__(self, tags_p, parent=None):
365         """
366         :param tags_p: A pointer to an underlying *notmuch_tags_t*
367              structure. These are not publically exposed, so a user
368              will almost never instantiate a :class:`Tags` object
369              herself. They are usually handed back as a result,
370              e.g. in :meth:`Database.get_all_tags`.  *tags_p* must be
371              valid, we will raise an :exc:`NotmuchError`
372              (STATUS.NULL_POINTER) if it is `None`.
373         :type tags_p: :class:`ctypes.c_void_p`
374         :param parent: The parent object (ie :class:`Database` or 
375              :class:`Message` these tags are derived from, and saves a
376              reference to it, so we can automatically delete the db object
377              once all derived objects are dead.
378         :TODO: Make the iterator optionally work more than once by
379                cache the tags in the Python object(?)
380         """
381         if tags_p is None:
382             NotmuchError(STATUS.NULL_POINTER)
383
384         self._tags = tags_p
385         #save reference to parent object so we keep it alive
386         self._parent = parent
387         logging.debug("Inited Tags derived from %s" %(repr(parent)))
388     
389     def __iter__(self):
390         """ Make Tags an iterator """
391         return self
392
393     def next(self):
394         if self._tags is None:
395             raise NotmuchError(STATUS.NOT_INITIALIZED)
396
397         if not nmlib.notmuch_tags_valid(self._tags):
398             self._tags = None
399             raise StopIteration
400
401         tag = Tags._get (self._tags)
402         nmlib.notmuch_tags_move_to_next(self._tags)
403         return tag
404
405     def __len__(self):
406         """len(:class:`Tags`) returns the number of contained tags
407
408         .. note:: As this iterates over the tags, we will not be able
409                to iterate over them again (as in retrieve them)! If
410                the tags have been exhausted already, this will raise a
411                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
412                subsequent attempts.
413         """
414         if self._tags is None:
415             raise NotmuchError(STATUS.NOT_INITIALIZED)
416
417         i=0
418         while nmlib.notmuch_tags_valid(self._msgs):
419             nmlib.notmuch_tags_move_to_next(self._msgs)
420             i += 1
421         self._tags = None
422         return i
423
424     def __str__(self):
425         """The str() representation of Tags() is a space separated list of tags
426
427         .. note:: As this iterates over the tags, we will not be able
428                to iterate over them again (as in retrieve them)! If
429                the tags have been exhausted already, this will raise a
430                :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
431                subsequent attempts.
432         """
433         return " ".join(self)
434
435     def __del__(self):
436         """Close and free the notmuch tags"""
437         if self._tags is not None:
438             logging.debug("Freeing the Tags now")
439             nmlib.notmuch_tags_destroy (self._tags)
440
441
442 #------------------------------------------------------------------------------
443 class Messages(object):
444     """Represents a list of notmuch messages
445
446     This object provides an iterator over a list of notmuch messages
447     (Technically, it provides a wrapper for the underlying
448     *notmuch_messages_t* structure). Do note that the underlying
449     library only provides a one-time iterator (it cannot reset the
450     iterator to the start). Thus iterating over the function will
451     "exhaust" the list of messages, and a subsequent iteration attempt
452     will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
453     note, that any function that uses iteration will also
454     exhaust the messages. So both::
455
456       for msg in msgs: print msg 
457
458     as well as::
459
460        number_of_msgs = len(msgs)
461
462     will "exhaust" the Messages. If you need to re-iterate over a list of
463     messages you will need to retrieve a new :class:`Messages` object.
464
465     Things are not as bad as it seems though, you can store and reuse
466     the single Message objects as often as you want as long as you
467     keep the parent Messages object around. (Recall that due to
468     hierarchical memory allocation, all derived Message objects will
469     be invalid when we delete the parent Messages() object, even if it
470     was already "exhausted".) So this works::
471
472       db   = Database()
473       msgs = Query(db,'').search_messages() #get a Messages() object
474       msglist = []
475       for m in msgs:
476          msglist.append(m)
477
478       # msgs is "exhausted" now and even len(msgs) will raise an exception.
479       # However it will be kept around until all retrieved Message() objects are
480       # also deleted. If you did e.g. an explicit del(msgs) here, the 
481       # following lines would fail.
482       
483       # You can reiterate over *msglist* however as often as you want. 
484       # It is simply a list with Message objects.
485
486       print (msglist[0].get_filename())
487       print (msglist[1].get_filename())
488       print (msglist[0].get_message_id())
489     """
490
491     #notmuch_tags_get
492     _get = nmlib.notmuch_messages_get
493     _get.restype = c_void_p
494
495     _collect_tags = nmlib.notmuch_messages_collect_tags
496     _collect_tags.restype = c_void_p
497
498     def __init__(self, msgs_p, parent=None):
499         """
500         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
501              structure. These are not publically exposed, so a user
502              will almost never instantiate a :class:`Messages` object
503              herself. They are usually handed back as a result,
504              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
505              valid, we will raise an :exc:`NotmuchError`
506              (STATUS.NULL_POINTER) if it is `None`.
507         :type msgs_p: :class:`ctypes.c_void_p`
508         :param parent: The parent object
509              (ie :class:`Query`) these tags are derived from. It saves
510              a reference to it, so we can automatically delete the db
511              object once all derived objects are dead.
512         :TODO: Make the iterator work more than once and cache the tags in 
513                the Python object.(?)
514         """
515         if msgs_p is None:
516             NotmuchError(STATUS.NULL_POINTER)
517
518         self._msgs = msgs_p
519         #store parent, so we keep them alive as long as self  is alive
520         self._parent = parent
521         logging.debug("Inited Messages derived from %s" %(str(parent)))
522
523     def collect_tags(self):
524         """Return the unique :class:`Tags` in the contained messages
525
526         :returns: :class:`Tags`
527         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
528
529         .. note:: :meth:`collect_tags` will iterate over the messages and
530           therefore will not allow further iterations.
531         """
532         if self._msgs is None:
533             raise NotmuchError(STATUS.NOT_INITIALIZED)
534
535         # collect all tags (returns NULL on error)
536         tags_p = Messages._collect_tags (self._msgs)
537         #reset _msgs as we iterated over it and can do so only once
538         self._msgs = None
539
540         if tags_p == None:
541             raise NotmuchError(STATUS.NULL_POINTER)
542         return Tags(tags_p, self)
543
544     def __iter__(self):
545         """ Make Messages an iterator """
546         return self
547
548     def next(self):
549         if self._msgs is None:
550             raise NotmuchError(STATUS.NOT_INITIALIZED)
551
552         if not nmlib.notmuch_messages_valid(self._msgs):
553             self._msgs = None
554             raise StopIteration
555
556         msg = Message(Messages._get (self._msgs), self)
557         nmlib.notmuch_messages_move_to_next(self._msgs)
558         return msg
559
560     def __len__(self):
561         """len(:class:`Messages`) returns the number of contained messages
562
563         .. note:: As this iterates over the messages, we will not be able to 
564                iterate over them again (as in retrieve them)!
565         """
566         if self._msgs is None:
567             raise NotmuchError(STATUS.NOT_INITIALIZED)
568
569         i=0
570         while nmlib.notmuch_messages_valid(self._msgs):
571             nmlib.notmuch_messages_move_to_next(self._msgs)
572             i += 1
573         self._msgs = None
574         return i
575
576
577
578     def __del__(self):
579         """Close and free the notmuch Messages"""
580         if self._msgs is not None:
581             logging.debug("Freeing the Messages now")
582             nmlib.notmuch_messages_destroy (self._msgs)
583
584
585 #------------------------------------------------------------------------------
586 class Message(object):
587     """Represents a single Email message
588
589     Technically, this wraps the underlying *notmuch_message_t* structure.
590     """
591
592     """notmuch_message_get_filename (notmuch_message_t *message)"""
593     _get_filename = nmlib.notmuch_message_get_filename
594     _get_filename.restype = c_char_p 
595     """notmuch_message_get_message_id (notmuch_message_t *message)"""
596     _get_message_id = nmlib.notmuch_message_get_message_id
597     _get_message_id.restype = c_char_p 
598
599     """notmuch_message_get_tags (notmuch_message_t *message)"""
600     _get_tags = nmlib.notmuch_message_get_tags
601     _get_tags.restype = c_void_p
602
603     _get_date = nmlib.notmuch_message_get_date
604     _get_date.restype = c_uint64
605
606     _get_header = nmlib.notmuch_message_get_header
607     _get_header.restype = c_char_p
608
609     def __init__(self, msg_p, parent=None):
610         """
611         :param msg_p: A pointer to an internal notmuch_message_t
612             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
613             STATUS.NULL_POINTER.
614         :param parent: A 'parent' object is passed which this message is
615               derived from. We save a reference to it, so we can
616               automatically delete the parent object once all derived
617               objects are dead.
618         """
619         if msg_p is None:
620             NotmuchError(STATUS.NULL_POINTER)
621         self._msg = msg_p
622         #keep reference to parent, so we keep it alive
623         self._parent = parent
624         logging.debug("Inited Message derived from %s" %(str(parent)))
625
626
627     def get_message_id(self):
628         """Return the message ID
629         
630         :returns: String with a message ID
631         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
632                     is not initialized.
633         """
634         if self._msg is None:
635             raise NotmuchError(STATUS.NOT_INITIALIZED)
636         return Message._get_message_id(self._msg)
637
638     def get_date(self):
639         """Returns time_t of the message date
640
641         For the original textual representation of the Date header from the
642         message call notmuch_message_get_header() with a header value of
643         "date".
644
645         :returns: a time_t timestamp
646         :rtype: c_unit64
647         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
648                     is not initialized.
649         """
650         if self._msg is None:
651             raise NotmuchError(STATUS.NOT_INITIALIZED)
652         return Message._get_date(self._msg)
653
654     def get_header(self, header):
655         """Returns a message header
656         
657         This returns any message header that is stored in the notmuch database.
658         This is only a selected subset of headers, which is currently:
659
660           TODO: add stored headers
661
662         :param header: The name of the header to be retrieved.
663                        It is not case-sensitive (TODO: confirm).
664         :type header: str
665         :returns: The header value as string
666         :exception: :exc:`NotmuchError`
667
668                     * STATUS.NOT_INITIALIZED if the message 
669                       is not initialized.
670                     * STATUS.NULL_POINTER, if no header was found
671         """
672         if self._msg is None:
673             raise NotmuchError(STATUS.NOT_INITIALIZED)
674
675         #Returns NULL if any error occurs.
676         header = Message._get_header (self._msg, header)
677         if header == None:
678             raise NotmuchError(STATUS.NULL_POINTER)
679         return header
680
681     def get_filename(self):
682         """Return the file path of the message file
683
684         :returns: Absolute file path & name of the message file
685         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
686               is not initialized.
687         """
688         if self._msg is None:
689             raise NotmuchError(STATUS.NOT_INITIALIZED)
690         return Message._get_filename(self._msg)
691
692     def get_tags(self):
693         """ Return the message tags
694
695         :returns: Message tags
696         :rtype: :class:`Tags`
697         :exception: :exc:`NotmuchError`
698
699                       * STATUS.NOT_INITIALIZED if the message 
700                         is not initialized.
701                       * STATUS.NULL_POINTER, on error
702         """
703         if self._msg is None:
704             raise NotmuchError(STATUS.NOT_INITIALIZED)
705
706         tags_p = Message._get_tags(self._msg)
707         if tags_p == None:
708             raise NotmuchError(STATUS.NULL_POINTER)
709         return Tags(tags_p, self)
710
711     def add_tag(self, tag):
712         """Add a tag to the given message
713
714         Adds a tag to the current message. The maximal tag length is defined in
715         the notmuch library and is currently 200 bytes.
716
717         :param tag: String with a 'tag' to be added.
718         :returns: STATUS.SUCCESS if the tag was successfully added.
719                   Raises an exception otherwise.
720         :exception: :exc:`NotmuchError`. They have the following meaning:
721
722                   STATUS.NULL_POINTER
723                     The 'tag' argument is NULL
724
725                   STATUS.TAG_TOO_LONG
726                     The length of 'tag' is too long 
727                     (exceeds Message.NOTMUCH_TAG_MAX)
728
729                   STATUS.READ_ONLY_DATABASE
730                     Database was opened in read-only mode so message cannot be 
731                     modified.
732
733                   STATUS.NOT_INITIALIZED
734                      The message has not been initialized.
735        """
736         if self._msg is None:
737             raise NotmuchError(STATUS.NOT_INITIALIZED)
738
739         status = nmlib.notmuch_message_add_tag (self._msg, tag)
740
741         if STATUS.SUCCESS == status:
742             # return on success
743             return status
744
745         raise NotmuchError(status)
746
747     def remove_tag(self, tag):
748         """Removes a tag from the given message
749
750         If the message has no such tag, this is a non-operation and
751         will report success anyway.
752
753         :param tag: String with a 'tag' to be removed.
754         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
755                   the message had no such tag.
756                   Raises an exception otherwise.
757         :exception: :exc:`NotmuchError`. They have the following meaning:
758
759                    STATUS.NULL_POINTER
760                      The 'tag' argument is NULL
761                    NOTMUCH_STATUS_TAG_TOO_LONG
762                      The length of 'tag' is too long
763                      (exceeds NOTMUCH_TAG_MAX)
764                    NOTMUCH_STATUS_READ_ONLY_DATABASE
765                      Database was opened in read-only mode so message cannot 
766                      be modified.
767                    STATUS.NOT_INITIALIZED
768                      The message has not been initialized.
769
770         """
771         if self._msg is None:
772             raise NotmuchError(STATUS.NOT_INITIALIZED)
773
774         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
775
776         if STATUS.SUCCESS == status:
777             # return on success
778             return status
779
780         raise NotmuchError(status)
781
782     def __str__(self):
783         """A message() is represented by a 1-line summary"""
784         msg = {}
785         msg['from'] = self.get_header('from')
786         msg['tags'] = str(self.get_tags())
787         msg['date'] = date.fromtimestamp(self.get_date())
788         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
789
790     def format_as_text(self):
791         """Output like notmuch show (Not implemented)"""
792         return str(self)
793
794     def __del__(self):
795         """Close and free the notmuch Message"""
796         if self._msg is not None:
797             logging.debug("Freeing the Message now")
798             nmlib.notmuch_message_destroy (self._msg)