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