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
7 from datetime import date
9 class Database(object):
10 """Represents a notmuch database (wraps notmuch_database_t)
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.
19 """Class attribute to cache user's default database"""
21 MODE = Enum(['READ_ONLY','READ_WRITE'])
22 """Constants: Mode in which to open the database"""
24 """notmuch_database_get_path (notmuch_database_t *database)"""
25 _get_path = nmlib.notmuch_database_get_path
26 _get_path.restype = c_char_p
28 """notmuch_database_get_version"""
29 _get_version = nmlib.notmuch_database_get_version
30 _get_version.restype = c_uint
32 """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
33 _open = nmlib.notmuch_database_open
34 _open.restype = c_void_p
36 """ notmuch_database_find_message """
37 _find_message = nmlib.notmuch_database_find_message
38 _find_message.restype = c_void_p
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
44 """ notmuch_database_create(const char *path):"""
45 _create = nmlib.notmuch_database_create
46 _create.restype = c_void_p
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`.
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.
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
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`
67 :exception: :exc:`NotmuchError` in case of failure.
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
82 def _verify_initialized_db(self):
83 """Raises a NotmuchError in case self._db is still None"""
85 raise NotmuchError(STATUS.NOT_INITIALIZED)
87 def create(self, path):
88 """Creates a new notmuch database
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.
97 :param path: A directory in which we should create the database.
100 :exception: :exc:`NotmuchError` in case of any failure
101 (after printing an error message on stderr).
103 if self._db is not None:
105 message="Cannot create db, this Database() already has an open one.")
107 res = Database._create(path, Database.MODE.READ_WRITE)
111 message="Could not create the specified database")
114 def open(self, path, mode= 0):
115 """Opens an existing database
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.
121 :param status: Open the database in read-only or read-write mode
122 :type status: :attr:`MODE`
124 :exception: Raises :exc:`NotmuchError` in case
125 of any failure (after printing an error message on stderr).
128 res = Database._open(path, mode)
132 message="Could not open the specified database")
136 """Returns the file path of an open database
138 Wraps notmuch_database_get_path"""
139 # Raise a NotmuchError if not initialized
140 self._verify_initialized_db()
142 return Database._get_path(self._db)
144 def get_version(self):
145 """Returns the database format version
147 :returns: The database version as positive integer
148 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
149 the database was not intitialized.
151 # Raise a NotmuchError if not initialized
152 self._verify_initialized_db()
154 return Database._get_version (self._db)
156 def needs_upgrade(self):
157 """Does this database need to be upgraded before writing to it?
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.
164 :returns: `True` or `False`
165 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
166 the database was not intitialized.
168 # Raise a NotmuchError if not initialized
169 self._verify_initialized_db()
171 return notmuch_database_needs_upgrade(self.db)
173 def add_message(self, filename):
174 """Adds a new message to the database
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
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.
186 :returns: On success, we return
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:
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.
199 :rtype: 2-tuple(:class:`Message`, STATUS)
201 :exception: Raises a :exc:`NotmuchError` with the following meaning.
202 If such an exception occurs, nothing was added to the database.
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
212 STATUS.NOT_INITIALIZED
213 The database has not been initialized.
215 # Raise a NotmuchError if not initialized
216 self._verify_initialized_db()
219 status = nmlib.notmuch_database_add_message(self._db,
223 if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
224 raise NotmuchError(status)
226 #construct Message() and return
227 msg = Message(msg_p, self)
230 def remove_message(self, filename):
231 """Removes a message from the given notmuch database
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.
240 :returns: A STATUS.* value with the following meaning:
243 The last filename was removed and the message was removed
245 STATUS.DUPLICATE_MESSAGE_ID
246 This filename was removed but the message persists in the
247 database with at least one other filename.
249 :exception: Raises a :exc:`NotmuchError` with the following meaning.
250 If such an exception occurs, nothing was removed from the database.
252 STATUS.READ_ONLY_DATABASE
253 Database was opened in read-only mode so no message can be
255 STATUS.NOT_INITIALIZED
256 The database has not been initialized.
258 # Raise a NotmuchError if not initialized
259 self._verify_initialized_db()
261 status = nmlib.notmuch_database_remove_message(self._db,
264 def find_message(self, msgid):
265 """Returns a :class:`Message` as identified by its message ID
267 Wraps the underlying *notmuch_database_find_message* function.
269 :param msgid: The message ID
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.
276 # Raise a NotmuchError if not initialized
277 self._verify_initialized_db()
279 msg_p = Database._find_message(self._db, msgid)
282 return Message(msg_p, self)
284 def get_all_tags(self):
285 """Returns :class:`Tags` with a list of all tags found in the database
287 :returns: :class:`Tags`
288 :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
290 # Raise a NotmuchError if not initialized
291 self._verify_initialized_db()
293 tags_p = Database._get_all_tags (self._db)
295 raise NotmuchError(STATUS.NULL_POINTER)
296 return Tags(tags_p, self)
298 def create_query(self, querystring):
299 """Returns a :class:`Query` derived from this database
301 This is a shorthand method for doing::
304 # Automatically frees the Database() when 'q' is deleted
306 q = Database(dbpath).create_query('from:"Biene Maja"')
308 # long version, which is functionally equivalent but will keep the
309 # Database in the 'db' variable around after we delete 'q':
311 db = Database(dbpath)
312 q = Query(db,'from:"Biene Maja"')
314 This function is a python extension and not in the underlying C API.
316 # Raise a NotmuchError if not initialized
317 self._verify_initialized_db()
319 return Query(self, querystring)
322 return "'Notmuch DB " + self.get_path() + "'"
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)
330 def _get_user_default_db(self):
331 """ Reads a user's notmuch config and returns his db location
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'))
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')
346 """Property returning a pointer to `notmuch_database_t` or `None`
348 This should normally not be needed by a user (and is not yet
349 guaranteed to remain stable in future versions).
353 #------------------------------------------------------------------------------
355 """Represents a search query on an opened :class:`Database`.
357 A query selects and filters a subset of messages from the notmuch
358 database we derive from.
360 Query() provides an instance attribute :attr:`sort`, which
361 contains the sort order (if specified via :meth:`set_sort`) or
364 Technically, it wraps the underlying *notmuch_query_t* struct.
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.
373 SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
374 """Constants: Sort order in which to return results"""
376 """notmuch_query_create"""
377 _create = nmlib.notmuch_query_create
378 _create.restype = c_void_p
380 """notmuch_query_search_threads"""
381 _search_threads = nmlib.notmuch_query_search_threads
382 _search_threads.restype = c_void_p
384 """notmuch_query_search_messages"""
385 _search_messages = nmlib.notmuch_query_search_messages
386 _search_messages.restype = c_void_p
389 """notmuch_query_count_messages"""
390 _count_messages = nmlib.notmuch_query_count_messages
391 _count_messages.restype = c_uint
393 def __init__(self, db, querystr):
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.
403 self.create(db, querystr)
405 def create(self, db, querystr):
406 """Creates a new query derived from a Database
408 This function is utilized by __init__() and usually does not need to
411 :param db: Database to create the query from.
412 :type db: :class:`Database`
413 :param querystr: The query string
416 :exception: :exc:`NotmuchError`
418 * STATUS.NOT_INITIALIZED if db is not inited
419 * STATUS.NULL_POINTER if the query creation failed
423 raise NotmuchError(STATUS.NOT_INITIALIZED)
424 # create reference to parent db to keep it alive
427 # create query, return None if too little mem available
428 query_p = Query._create(db.db_p, querystr)
430 NotmuchError(STATUS.NULL_POINTER)
431 self._query = query_p
433 def set_sort(self, sort):
434 """Set the sort order future results will be delivered in
436 Wraps the underlying *notmuch_query_set_sort* function.
438 :param sort: Sort order (see :attr:`Query.SORT`)
440 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
443 if self._query is None:
444 raise NotmuchError(STATUS.NOT_INITIALIZED)
447 nmlib.notmuch_query_set_sort(self._query, sort)
449 def search_threads(self):
450 """Execute a query for threads
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.
456 Technically, it wraps the underlying
457 *notmuch_query_search_threads* function.
459 :returns: :class:`Threads`
460 :exception: :exc:`NotmuchError`
462 * STATUS.NOT_INITIALIZED if query is not inited
463 * STATUS.NULL_POINTER if search_messages failed
465 if self._query is None:
466 raise NotmuchError(STATUS.NOT_INITIALIZED)
468 threads_p = Query._search_threads(self._query)
470 if threads_p is None:
471 NotmuchError(STATUS.NULL_POINTER)
473 return Threads(threads_p,self)
475 def search_messages(self):
476 """Filter messages according to the query and return
477 :class:`Messages` in the defined sort order
479 Technically, it wraps the underlying
480 *notmuch_query_search_messages* function.
482 :returns: :class:`Messages`
483 :exception: :exc:`NotmuchError`
485 * STATUS.NOT_INITIALIZED if query is not inited
486 * STATUS.NULL_POINTER if search_messages failed
488 if self._query is None:
489 raise NotmuchError(STATUS.NOT_INITIALIZED)
491 msgs_p = Query._search_messages(self._query)
494 NotmuchError(STATUS.NULL_POINTER)
496 return Messages(msgs_p,self)
498 def count_messages(self):
499 """Estimate the number of messages matching the query
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.
508 :returns: :class:`Messages`
509 :exception: :exc:`NotmuchError`
511 * STATUS.NOT_INITIALIZED if query is not inited
513 if self._query is None:
514 raise NotmuchError(STATUS.NOT_INITIALIZED)
516 return Query._count_messages(self._query)
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)
524 #------------------------------------------------------------------------------
525 class Messages(object):
526 """Represents a list of notmuch messages
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::
538 for msg in msgs: print msg
542 number_of_msgs = len(msgs)
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.
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::
555 msgs = Query(db,'').search_messages() #get a Messages() object
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.
565 # You can reiterate over *msglist* however as often as you want.
566 # It is simply a list with Message objects.
568 print (msglist[0].get_filename())
569 print (msglist[1].get_filename())
570 print (msglist[0].get_message_id())
574 _get = nmlib.notmuch_messages_get
575 _get.restype = c_void_p
577 _collect_tags = nmlib.notmuch_messages_collect_tags
578 _collect_tags.restype = c_void_p
580 def __init__(self, msgs_p, parent=None):
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.(?)
598 NotmuchError(STATUS.NULL_POINTER)
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)))
605 def collect_tags(self):
606 """Return the unique :class:`Tags` in the contained messages
608 :returns: :class:`Tags`
609 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
611 .. note:: :meth:`collect_tags` will iterate over the messages and
612 therefore will not allow further iterations.
614 if self._msgs is None:
615 raise NotmuchError(STATUS.NOT_INITIALIZED)
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
623 raise NotmuchError(STATUS.NULL_POINTER)
624 return Tags(tags_p, self)
627 """ Make Messages an iterator """
631 if self._msgs is None:
632 raise NotmuchError(STATUS.NOT_INITIALIZED)
634 if not nmlib.notmuch_messages_valid(self._msgs):
638 msg = Message(Messages._get (self._msgs), self)
639 nmlib.notmuch_messages_move_to_next(self._msgs)
643 """len(:class:`Messages`) returns the number of contained messages
645 .. note:: As this iterates over the messages, we will not be able to
646 iterate over them again! So this will fail::
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
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.
660 if self._msgs is None:
661 raise NotmuchError(STATUS.NOT_INITIALIZED)
664 while nmlib.notmuch_messages_valid(self._msgs):
665 nmlib.notmuch_messages_move_to_next(self._msgs)
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)
679 #------------------------------------------------------------------------------
680 class Message(object):
681 """Represents a single Email message
683 Technically, this wraps the underlying *notmuch_message_t* structure.
686 """notmuch_message_get_filename (notmuch_message_t *message)"""
687 _get_filename = nmlib.notmuch_message_get_filename
688 _get_filename.restype = c_char_p
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
694 """notmuch_message_get_thread_id"""
695 _get_thread_id = nmlib.notmuch_message_get_thread_id
696 _get_thread_id.restype = c_char_p
698 """notmuch_message_get_replies"""
699 _get_replies = nmlib.notmuch_message_get_replies
700 _get_replies.restype = c_void_p
702 """notmuch_message_get_tags (notmuch_message_t *message)"""
703 _get_tags = nmlib.notmuch_message_get_tags
704 _get_tags.restype = c_void_p
706 _get_date = nmlib.notmuch_message_get_date
707 _get_date.restype = c_uint64
709 _get_header = nmlib.notmuch_message_get_header
710 _get_header.restype = c_char_p
712 def __init__(self, msg_p, parent=None):
714 :param msg_p: A pointer to an internal notmuch_message_t
715 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
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
723 NotmuchError(STATUS.NULL_POINTER)
725 #keep reference to parent, so we keep it alive
726 self._parent = parent
727 logging.debug("Inited Message derived from %s" %(str(parent)))
730 def get_message_id(self):
731 """Returns the message ID
733 :returns: String with a message ID
734 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
737 if self._msg is None:
738 raise NotmuchError(STATUS.NOT_INITIALIZED)
739 return Message._get_message_id(self._msg)
741 def get_thread_id(self):
742 """Returns the thread ID
744 The returned string belongs to 'message' will only be valid for as
745 long as the message is valid.
747 This function will not return None since Notmuch ensures that every
748 message belongs to a single thread.
750 :returns: String with a thread ID
751 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
754 if self._msg is None:
755 raise NotmuchError(STATUS.NOT_INITIALIZED)
757 return Message._get_thread_id (self._msg);
759 def get_replies(self):
760 """Gets all direct replies to this message as :class:`Messages` iterator
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
771 :returns: :class:`Messages` or `None` if there are no replies to
773 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
776 if self._msg is None:
777 raise NotmuchError(STATUS.NOT_INITIALIZED)
779 msgs_p = Message._get_replies(self._msg);
784 return Messages(msgs_p,self)
787 """Returns time_t of the message date
789 For the original textual representation of the Date header from the
790 message call notmuch_message_get_header() with a header value of
793 :returns: A time_t timestamp.
795 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
798 if self._msg is None:
799 raise NotmuchError(STATUS.NOT_INITIALIZED)
800 return Message._get_date(self._msg)
802 def get_header(self, header):
803 """Returns a message header
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:
808 TODO: add stored headers
810 :param header: The name of the header to be retrieved.
811 It is not case-sensitive (TODO: confirm).
813 :returns: The header value as string
814 :exception: :exc:`NotmuchError`
816 * STATUS.NOT_INITIALIZED if the message
818 * STATUS.NULL_POINTER, if no header was found
820 if self._msg is None:
821 raise NotmuchError(STATUS.NOT_INITIALIZED)
823 #Returns NULL if any error occurs.
824 header = Message._get_header (self._msg, header)
826 raise NotmuchError(STATUS.NULL_POINTER)
829 def get_filename(self):
830 """Returns the file path of the message file
832 :returns: Absolute file path & name of the message file
833 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
836 if self._msg is None:
837 raise NotmuchError(STATUS.NOT_INITIALIZED)
838 return Message._get_filename(self._msg)
841 """Returns the message tags
843 :returns: A :class:`Tags` iterator.
844 :exception: :exc:`NotmuchError`
846 * STATUS.NOT_INITIALIZED if the message
848 * STATUS.NULL_POINTER, on error
850 if self._msg is None:
851 raise NotmuchError(STATUS.NOT_INITIALIZED)
853 tags_p = Message._get_tags(self._msg)
855 raise NotmuchError(STATUS.NULL_POINTER)
856 return Tags(tags_p, self)
858 def add_tag(self, tag):
859 """Adds a tag to the given message
861 Adds a tag to the current message. The maximal tag length is defined in
862 the notmuch library and is currently 200 bytes.
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:
870 The 'tag' argument is NULL
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
877 STATUS.NOT_INITIALIZED
878 The message has not been initialized.
880 if self._msg is None:
881 raise NotmuchError(STATUS.NOT_INITIALIZED)
883 status = nmlib.notmuch_message_add_tag (self._msg, tag)
885 if STATUS.SUCCESS == status:
889 raise NotmuchError(status)
891 def remove_tag(self, tag):
892 """Removes a tag from the given message
894 If the message has no such tag, this is a non-operation and
895 will report success anyway.
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:
904 The 'tag' argument is NULL
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
911 STATUS.NOT_INITIALIZED
912 The message has not been initialized.
914 if self._msg is None:
915 raise NotmuchError(STATUS.NOT_INITIALIZED)
917 status = nmlib.notmuch_message_remove_tag(self._msg, tag)
919 if STATUS.SUCCESS == status:
923 raise NotmuchError(status)
925 def remove_all_tags(self):
926 """Removes all tags from the given message.
928 See :meth:`freeze` for an example showing how to safely
931 :returns: STATUS.SUCCESS if the tags were successfully removed.
932 Raises an exception otherwise.
933 :exception: :exc:`NotmuchError`. They have the following meaning:
935 STATUS.READ_ONLY_DATABASE
936 Database was opened in read-only mode so message cannot
938 STATUS.NOT_INITIALIZED
939 The message has not been initialized.
941 if self._msg is None:
942 raise NotmuchError(STATUS.NOT_INITIALIZED)
944 status = nmlib.notmuch_message_remove_all_tags(self._msg)
946 if STATUS.SUCCESS == status:
950 raise NotmuchError(status)
953 """Freezes the current state of 'message' within the database
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.
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.
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::
968 msg.remove_all_tags()
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.
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`.
982 :returns: STATUS.SUCCESS if the message was successfully frozen.
983 Raises an exception otherwise.
984 :exception: :exc:`NotmuchError`. They have the following meaning:
986 STATUS.READ_ONLY_DATABASE
987 Database was opened in read-only mode so message cannot
989 STATUS.NOT_INITIALIZED
990 The message has not been initialized.
992 if self._msg is None:
993 raise NotmuchError(STATUS.NOT_INITIALIZED)
995 status = nmlib.notmuch_message_freeze(self._msg)
997 if STATUS.SUCCESS == status:
1001 raise NotmuchError(status)
1004 """Thaws the current 'message'
1006 Thaw the current 'message', synchronizing any changes that may have
1007 occurred while 'message' was frozen into the notmuch database.
1009 See :meth:`freeze` for an example of how to use this
1010 function to safely provide tag changes.
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.
1016 :returns: STATUS.SUCCESS if the message was successfully frozen.
1017 Raises an exception otherwise.
1018 :exception: :exc:`NotmuchError`. They have the following meaning:
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.
1027 if self._msg is None:
1028 raise NotmuchError(STATUS.NOT_INITIALIZED)
1030 status = nmlib.notmuch_message_thaw(self._msg)
1032 if STATUS.SUCCESS == status:
1036 raise NotmuchError(status)
1040 """A message() is represented by a 1-line summary"""
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)
1049 def format_as_text(self):
1050 """Output like notmuch show (Not implemented)"""
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)