2 from ctypes import c_int, c_char_p, c_void_p, c_uint64
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
5 from datetime import date
7 class Database(object):
8 """Represents a notmuch database (wraps notmuch_database_t)
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.
16 MODE = Enum(['READ_ONLY','READ_WRITE'])
17 """Constants: Mode in which to open the database"""
20 """Class attribute to cache user's default database"""
22 """notmuch_database_get_path (notmuch_database_t *database)"""
23 _get_path = nmlib.notmuch_database_get_path
24 _get_path.restype = c_char_p
26 """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
27 _open = nmlib.notmuch_database_open
28 _open.restype = c_void_p
30 """ notmuch_database_find_message """
31 _find_message = nmlib.notmuch_database_find_message
32 _find_message.restype = c_void_p
34 """notmuch_database_get_all_tags (notmuch_database_t *database)"""
35 _get_all_tags = nmlib.notmuch_database_get_all_tags
36 _get_all_tags.restype = c_void_p
38 """ notmuch_database_create(const char *path):"""
39 _create = nmlib.notmuch_database_create
40 _create.restype = c_void_p
42 def __init__(self, path=None, create=False, mode= MODE.READ_ONLY):
43 """If *path* is *None*, we will try to read a users notmuch
44 configuration and use his default database. If *create* is `True`,
45 the database will always be created in
46 :attr:`MODE`.READ_WRITE mode.
48 :param path: Directory to open/create the database in (see
49 above for behavior if `None`)
50 :type path: `str` or `None`
51 :param create: Pass `False` to open an existing, `True` to create a new
54 :param mode: Mode to open a database in. Is always
55 :attr:`MODE`.READ_WRITE when creating a new one.
56 :type mode: :attr:`MODE`
58 :exception: :exc:`NotmuchError` in case of failure.
62 # no path specified. use a user's default database
63 if Database._std_db_path is None:
64 #the following line throws a NotmuchError if it fails
65 Database._std_db_path = self._get_user_default_db()
66 path = Database._std_db_path
73 def create(self, path):
74 """Creates a new notmuch database
76 This function is used by __init__() and usually does not need
77 to be called directly. It wraps the underlying
78 *notmuch_database_create* function and creates a new notmuch
79 database at *path*. It will always return a database in
80 :attr:`MODE`.READ_WRITE mode as creating an empty database for
81 reading only does not make a great deal of sense.
83 :param path: A directory in which we should create the database.
86 :exception: :exc:`NotmuchError` in case of any failure
87 (after printing an error message on stderr).
89 if self._db is not None:
91 message="Cannot create db, this Database() already has an open one.")
93 res = Database._create(path, MODE.READ_WRITE)
97 message="Could not create the specified database")
100 def open(self, path, mode= MODE.READ_ONLY):
101 """Opens an existing database
103 This function is used by __init__() and usually does not need
104 to be called directly. It wraps the underlying
105 *notmuch_database_open* function.
107 :param status: Open the database in read-only or read-write mode
108 :type status: :attr:`MODE`
110 :exception: Raises :exc:`NotmuchError` in case
111 of any failure (after printing an error message on stderr).
114 res = Database._open(path, mode)
118 message="Could not open the specified database")
122 """Returns the file path of an open database
124 Wraps notmuch_database_get_path"""
125 return Database._get_path(self._db)
127 def find_message(self, msgid):
128 """Returns a :class:`Message` as identified by its message ID
130 Wraps the underlying *notmuch_database_find_message* function.
132 :param msgid: The message ID
134 :returns: :class:`Message` or `None` if no message is found or if an
135 out-of-memory situation occurs.
136 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
137 the database was not intitialized.
140 raise NotmuchError(STATUS.NOT_INITIALIZED)
141 msg_p = Database._find_message(self._db, msgid)
144 return Message(msg_p, self)
146 def get_all_tags(self):
147 """Returns :class:`Tags` with a list of all tags found in the database
149 :returns: :class:`Tags`
150 :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
153 raise NotmuchError(STATUS.NOT_INITIALIZED)
155 tags_p = Database._get_all_tags (self._db)
157 raise NotmuchError(STATUS.NULL_POINTER)
158 return Tags(tags_p, self)
161 return "'Notmuch DB " + self.get_path() + "'"
164 """Close and free the notmuch database if needed"""
165 if self._db is not None:
166 logging.debug("Freeing the database now")
167 nmlib.notmuch_database_close(self._db)
169 def _get_user_default_db(self):
170 """ Reads a user's notmuch config and returns his db location
172 Throws a NotmuchError if it cannot find it"""
173 from ConfigParser import SafeConfigParser
175 config = SafeConfigParser()
176 config.read(os.path.expanduser('~/.notmuch-config'))
177 if not config.has_option('database','path'):
178 raise NotmuchError(message=
179 "No DB path specified and no user default found")
180 return config.get('database','path')
184 """Property returning a pointer to the notmuch_database_t or `None`
186 This should normally not be needed by a user."""
189 #------------------------------------------------------------------------------
191 """ Represents a search query on an opened :class:`Database`.
193 A query selects and filters a subset of messages from the notmuch
194 database we derive from.
196 Technically, it wraps the underlying *notmuch_query_t* struct.
198 .. note:: Do remember that as soon as we tear down this object,
199 all underlying derived objects such as threads,
200 messages, tags etc will be freed by the underlying library
201 as well. Accessing these objects will lead to segfaults and
202 other unexpected behavior. See above for more details.
205 SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
206 """Constants: Sort order in which to return results"""
208 """notmuch_query_create"""
209 _create = nmlib.notmuch_query_create
210 _create.restype = c_void_p
212 """notmuch_query_search_messages"""
213 _search_messages = nmlib.notmuch_query_search_messages
214 _search_messages.restype = c_void_p
216 def __init__(self, db, querystr):
218 :param db: An open database which we derive the Query from.
219 :type db: :class:`Database`
220 :param querystr: The query string for the message.
225 self.create(db, querystr)
227 def create(self, db, querystr):
228 """Creates a new query derived from a Database.
230 This function is utilized by __init__() and usually does not need to
233 :param db: Database to create the query from.
234 :type db: :class:`Database`
235 :param querystr: The query string
238 :exception: :exc:`NotmuchError`
240 * STATUS.NOT_INITIALIZED if db is not inited
241 * STATUS.NULL_POINTER if the query creation failed
245 raise NotmuchError(STATUS.NOT_INITIALIZED)
246 # create reference to parent db to keep it alive
249 # create query, return None if too little mem available
250 query_p = Query._create(db.db_p, querystr)
252 NotmuchError(STATUS.NULL_POINTER)
253 self._query = query_p
255 def set_sort(self, sort):
256 """Set the sort order future results will be delivered in
258 Wraps the underlying *notmuch_query_set_sort* function.
260 :param sort: Sort order (see :attr:`Query.SORT`)
262 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
265 if self._query is None:
266 raise NotmuchError(STATUS.NOT_INITIALIZED)
268 nmlib.notmuch_query_set_sort(self._query, sort)
270 def search_messages(self):
271 """Filter messages according to the query and return
272 :class:`Messages` in the defined sort order
274 Technically, it wraps the underlying
275 *notmuch_query_search_messages* function.
277 :returns: :class:`Messages`
278 :exception: :exc:`NotmuchError`
280 * STATUS.NOT_INITIALIZED if query is not inited
281 * STATUS.NULL_POINTER if search_messages failed
283 if self._query is None:
284 raise NotmuchError(STATUS.NOT_INITIALIZED)
286 msgs_p = Query._search_messages(self._query)
289 NotmuchError(STATUS.NULL_POINTER)
291 return Messages(msgs_p,self)
295 """Close and free the Query"""
296 if self._query is not None:
297 logging.debug("Freeing the Query now")
298 nmlib.notmuch_query_destroy (self._query)
300 #------------------------------------------------------------------------------
302 """Represents a list of notmuch tags
304 This object provides an iterator over a list of notmuch tags. Do
305 note that the underlying library only provides a one-time iterator
306 (it cannot reset the iterator to the start). Thus iterating over
307 the function will "exhaust" the list of tags, and a subsequent
308 iteration attempt will raise a :exc:`NotmuchError`
309 STATUS.NOT_INITIALIZED. Also note, that any function that uses
310 iteration (nearly all) will also exhaust the tags. So both::
312 for tag in tags: print tag
316 number_of_tags = len(tags)
320 #str() iterates over all tags to construct a space separated list
323 will "exhaust" the Tags. If you need to re-iterate over a list of
324 tags you will need to retrieve a new :class:`Tags` object.
328 _get = nmlib.notmuch_tags_get
329 _get.restype = c_char_p
331 def __init__(self, tags_p, parent=None):
333 :param tags_p: A pointer to an underlying *notmuch_tags_t*
334 structure. These are not publically exposed, so a user
335 will almost never instantiate a :class:`Tags` object
336 herself. They are usually handed back as a result,
337 e.g. in :meth:`Database.get_all_tags`. *tags_p* must be
338 valid, we will raise an :exc:`NotmuchError`
339 (STATUS.NULL_POINTER) if it is `None`.
340 :type tags_p: :class:`ctypes.c_void_p`
341 :param parent: The parent object (ie :class:`Database` or
342 :class:`Message` these tags are derived from, and saves a
343 reference to it, so we can automatically delete the db object
344 once all derived objects are dead.
345 :TODO: Make the iterator optionally work more than once by
346 cache the tags in the Python object(?)
349 NotmuchError(STATUS.NULL_POINTER)
352 #save reference to parent object so we keep it alive
353 self._parent = parent
354 logging.debug("Inited Tags derived from %s" %(repr(parent)))
357 """ Make Tags an iterator """
361 if self._tags is None:
362 raise NotmuchError(STATUS.NOT_INITIALIZED)
364 if not nmlib.notmuch_tags_valid(self._tags):
368 tag = Tags._get (self._tags)
369 nmlib.notmuch_tags_move_to_next(self._tags)
373 """len(:class:`Tags`) returns the number of contained tags
375 .. note:: As this iterates over the tags, we will not be able
376 to iterate over them again (as in retrieve them)! If
377 the tags have been exhausted already, this will raise a
378 :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
381 if self._tags is None:
382 raise NotmuchError(STATUS.NOT_INITIALIZED)
385 while nmlib.notmuch_tags_valid(self._msgs):
386 nmlib.notmuch_tags_move_to_next(self._msgs)
392 """The str() representation of Tags() is a space separated list of tags
394 .. note:: As this iterates over the tags, we will not be able
395 to iterate over them again (as in retrieve them)! If
396 the tags have been exhausted already, this will raise a
397 :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
400 return " ".join(self)
403 """Close and free the notmuch tags"""
404 if self._tags is not None:
405 logging.debug("Freeing the Tags now")
406 nmlib.notmuch_tags_destroy (self._tags)
409 #------------------------------------------------------------------------------
410 class Messages(object):
411 """Represents a list of notmuch messages
413 This object provides an iterator over a list of notmuch messages
414 (Technically, it provides a wrapper for the underlying
415 *notmuch_messages_t* structure). Do note that the underlying
416 library only provides a one-time iterator (it cannot reset the
417 iterator to the start). Thus iterating over the function will
418 "exhaust" the list of messages, and a subsequent iteration attempt
419 will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
420 note, that any function that uses iteration will also
421 exhaust the messages. So both::
423 for msg in msgs: print msg
427 number_of_msgs = len(msgs)
429 will "exhaust" the Messages. If you need to re-iterate over a list of
430 messages you will need to retrieve a new :class:`Messages` object.
432 Things are not as bad as it seems though, you can store and reuse
433 the single Message objects as often as you want as long as you
434 keep the parent Messages object around. (Recall that due to
435 hierarchical memory allocation, all derived Message objects will
436 be invalid when we delete the parent Messages() object, even if it
437 was already "exhausted".) So this works::
440 msgs = Query(db,'').search_messages() #get a Messages() object
445 # msgs is "exhausted" now and even len(msgs) will raise an exception.
446 # However it will be kept around until all retrieved Message() objects are
447 # also deleted. If you did e.g. an explicit del(msgs) here, the
448 # following lines would fail.
450 # You can reiterate over *msglist* however as often as you want.
451 # It is simply a list with Message objects.
453 print (msglist[0].get_filename())
454 print (msglist[1].get_filename())
455 print (msglist[0].get_message_id())
459 _get = nmlib.notmuch_messages_get
460 _get.restype = c_void_p
462 _collect_tags = nmlib.notmuch_messages_collect_tags
463 _collect_tags.restype = c_void_p
465 def __init__(self, msgs_p, parent=None):
467 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
468 structure. These are not publically exposed, so a user
469 will almost never instantiate a :class:`Messages` object
470 herself. They are usually handed back as a result,
471 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
472 valid, we will raise an :exc:`NotmuchError`
473 (STATUS.NULL_POINTER) if it is `None`.
474 :type msgs_p: :class:`ctypes.c_void_p`
475 :param parent: The parent object
476 (ie :class:`Query`) these tags are derived from. It saves
477 a reference to it, so we can automatically delete the db
478 object once all derived objects are dead.
479 :TODO: Make the iterator work more than once and cache the tags in
480 the Python object.(?)
483 NotmuchError(STATUS.NULL_POINTER)
486 #store parent, so we keep them alive as long as self is alive
487 self._parent = parent
488 logging.debug("Inited Messages derived from %s" %(str(parent)))
490 def collect_tags(self):
491 """Return the unique :class:`Tags` in the contained messages
493 :returns: :class:`Tags`
494 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
496 .. note:: :meth:`collect_tags` will iterate over the messages and
497 therefore will not allow further iterations.
499 if self._msgs is None:
500 raise NotmuchError(STATUS.NOT_INITIALIZED)
502 # collect all tags (returns NULL on error)
503 tags_p = Messages._collect_tags (self._msgs)
504 #reset _msgs as we iterated over it and can do so only once
508 raise NotmuchError(STATUS.NULL_POINTER)
509 return Tags(tags_p, self)
512 """ Make Messages an iterator """
516 if self._msgs is None:
517 raise NotmuchError(STATUS.NOT_INITIALIZED)
519 if not nmlib.notmuch_messages_valid(self._msgs):
523 msg = Message(Messages._get (self._msgs), self)
524 nmlib.notmuch_messages_move_to_next(self._msgs)
528 """len(:class:`Messages`) returns the number of contained messages
530 .. note:: As this iterates over the messages, we will not be able to
531 iterate over them again (as in retrieve them)!
533 if self._msgs is None:
534 raise NotmuchError(STATUS.NOT_INITIALIZED)
537 while nmlib.notmuch_messages_valid(self._msgs):
538 nmlib.notmuch_messages_move_to_next(self._msgs)
546 """Close and free the notmuch Messages"""
547 if self._msgs is not None:
548 logging.debug("Freeing the Messages now")
549 nmlib.notmuch_messages_destroy (self._msgs)
552 #------------------------------------------------------------------------------
553 class Message(object):
554 """Represents a single Email message
556 Technically, this wraps the underlying *notmuch_message_t* structure.
559 """notmuch_message_get_filename (notmuch_message_t *message)"""
560 _get_filename = nmlib.notmuch_message_get_filename
561 _get_filename.restype = c_char_p
562 """notmuch_message_get_message_id (notmuch_message_t *message)"""
563 _get_message_id = nmlib.notmuch_message_get_message_id
564 _get_message_id.restype = c_char_p
566 """notmuch_message_get_tags (notmuch_message_t *message)"""
567 _get_tags = nmlib.notmuch_message_get_tags
568 _get_tags.restype = c_void_p
570 _get_date = nmlib.notmuch_message_get_date
571 _get_date.restype = c_uint64
573 _get_header = nmlib.notmuch_message_get_header
574 _get_header.restype = c_char_p
576 def __init__(self, msg_p, parent=None):
578 :param msg_p: A pointer to an internal notmuch_message_t
579 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
581 :param parent: A 'parent' object is passed which this message is
582 derived from. We save a reference to it, so we can
583 automatically delete the parent object once all derived
587 NotmuchError(STATUS.NULL_POINTER)
589 #keep reference to parent, so we keep it alive
590 self._parent = parent
591 logging.debug("Inited Message derived from %s" %(str(parent)))
594 def get_message_id(self):
595 """Return the message ID
597 :returns: String with a message ID
598 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
601 if self._msg is None:
602 raise NotmuchError(STATUS.NOT_INITIALIZED)
603 return Message._get_message_id(self._msg)
606 """Returns time_t of the message date
608 For the original textual representation of the Date header from the
609 message call notmuch_message_get_header() with a header value of
612 :returns: a time_t timestamp
614 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
617 if self._msg is None:
618 raise NotmuchError(STATUS.NOT_INITIALIZED)
619 return Message._get_date(self._msg)
621 def get_header(self, header):
622 """Returns a message header
624 This returns any message header that is stored in the notmuch database.
625 This is only a selected subset of headers, which is currently:
627 TODO: add stored headers
629 :param header: The name of the header to be retrieved.
630 It is not case-sensitive (TODO: confirm).
632 :returns: The header value as string
633 :exception: :exc:`NotmuchError`
635 * STATUS.NOT_INITIALIZED if the message
637 * STATUS.NULL_POINTER, if no header was found
639 if self._msg is None:
640 raise NotmuchError(STATUS.NOT_INITIALIZED)
642 #Returns NULL if any error occurs.
643 header = Message._get_header (self._msg, header)
645 raise NotmuchError(STATUS.NULL_POINTER)
648 def get_filename(self):
649 """Return the file path of the message file
651 :returns: Absolute file path & name of the message file
652 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
655 if self._msg is None:
656 raise NotmuchError(STATUS.NOT_INITIALIZED)
657 return Message._get_filename(self._msg)
660 """ Return the message tags
662 :returns: Message tags
663 :rtype: :class:`Tags`
664 :exception: :exc:`NotmuchError`
666 * STATUS.NOT_INITIALIZED if the message
668 * STATUS.NULL_POINTER, on error
670 if self._msg is None:
671 raise NotmuchError(STATUS.NOT_INITIALIZED)
673 tags_p = Message._get_tags(self._msg)
675 raise NotmuchError(STATUS.NULL_POINTER)
676 return Tags(tags_p, self)
678 def add_tag(self, tag):
679 """Add a tag to the given message
681 Adds a tag to the current message. The maximal tag length is defined in
682 the notmuch library and is currently 200 bytes.
684 :param tag: String with a 'tag' to be added.
685 :returns: STATUS.SUCCESS if the tag was successfully added.
686 Raises an exception otherwise.
687 :exception: :exc:`NotmuchError`. They have the following meaning:
690 The 'tag' argument is NULL
693 The length of 'tag' is too long
694 (exceeds Message.NOTMUCH_TAG_MAX)
696 STATUS.READ_ONLY_DATABASE
697 Database was opened in read-only mode so message cannot be
700 STATUS.NOT_INITIALIZED
701 The message has not been initialized.
703 if self._msg is None:
704 raise NotmuchError(STATUS.NOT_INITIALIZED)
706 status = nmlib.notmuch_message_add_tag (self._msg, tag)
708 if STATUS.SUCCESS == status:
712 raise NotmuchError(status)
714 def remove_tag(self, tag):
715 """Removes a tag from the given message
717 :param tag: String with a 'tag' to be removed.
718 :returns: STATUS.SUCCESS if the tag was successfully removed.
719 Raises an exception otherwise.
720 :exception: :exc:`NotmuchError`. They have the following meaning:
723 The 'tag' argument is NULL
724 NOTMUCH_STATUS_TAG_TOO_LONG
725 The length of 'tag' is too long
726 (exceeds NOTMUCH_TAG_MAX)
727 NOTMUCH_STATUS_READ_ONLY_DATABASE
728 Database was opened in read-only mode so message cannot
730 STATUS.NOT_INITIALIZED
731 The message has not been initialized.
734 if self._msg is None:
735 raise NotmuchError(STATUS.NOT_INITIALIZED)
737 status = nmlib.notmuch_message_remove_tag(self._msg, tag)
739 if STATUS.SUCCESS == status:
743 raise NotmuchError(status)
746 """A message() is represented by a 1-line summary"""
748 msg['from'] = self.get_header('from')
749 msg['tags'] = str(self.get_tags())
750 msg['date'] = date.fromtimestamp(self.get_date())
751 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
753 def format_as_text(self):
754 """Output like notmuch show (Not implemented)"""
758 """Close and free the notmuch Message"""
759 if self._msg is not None:
760 logging.debug("Freeing the Message now")
761 nmlib.notmuch_message_destroy (self._msg)