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 queries, 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 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 """Wrapper around notmuch_message_t"""
556 """notmuch_message_get_filename (notmuch_message_t *message)"""
557 _get_filename = nmlib.notmuch_message_get_filename
558 _get_filename.restype = c_char_p
559 """notmuch_message_get_message_id (notmuch_message_t *message)"""
560 _get_message_id = nmlib.notmuch_message_get_message_id
561 _get_message_id.restype = c_char_p
563 """notmuch_message_get_tags (notmuch_message_t *message)"""
564 _get_tags = nmlib.notmuch_message_get_tags
565 _get_tags.restype = c_void_p
567 _get_date = nmlib.notmuch_message_get_date
568 _get_date.restype = c_uint64
570 _get_header = nmlib.notmuch_message_get_header
571 _get_header.restype = c_char_p
573 def __init__(self, msg_p, parent=None):
575 msg_p is a pointer to an notmuch_message_t Structure. If it is None,
576 we will raise an NotmuchError(STATUS.NULL_POINTER).
578 Is a 'parent' object is passed which this message is derived from,
579 we save a reference to it, so we can automatically delete the parent
580 object once all derived objects are dead.
583 NotmuchError(STATUS.NULL_POINTER)
585 #keep reference to parent, so we keep it alive
586 self._parent = parent
587 logging.debug("Inited Message derived from %s" %(str(parent)))
590 def get_message_id(self):
591 """ return the msg id
593 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
595 if self._msg is None:
596 raise NotmuchError(STATUS.NOT_INITIALIZED)
597 return Message._get_message_id(self._msg)
600 """returns time_t of the message date
602 For the original textual representation of the Date header from the
603 message call notmuch_message_get_header() with a header value of
605 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
607 if self._msg is None:
608 raise NotmuchError(STATUS.NOT_INITIALIZED)
609 return Message._get_date(self._msg)
611 def get_header(self, header):
612 """ TODO document me"""
613 if self._msg is None:
614 raise NotmuchError(STATUS.NOT_INITIALIZED)
616 #Returns NULL if any error occurs.
617 header = Message._get_header (self._msg, header)
619 raise NotmuchError(STATUS.NULL_POINTER)
622 def get_filename(self):
623 """ return the msg filename
625 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
627 if self._msg is None:
628 raise NotmuchError(STATUS.NOT_INITIALIZED)
629 return Message._get_filename(self._msg)
632 """ return the msg tags
634 Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
635 Raises NotmuchError(STATUS.NULL_POINTER) on error.
637 if self._msg is None:
638 raise NotmuchError(STATUS.NOT_INITIALIZED)
640 tags_p = Message._get_tags(self._msg)
642 raise NotmuchError(STATUS.NULL_POINTER)
643 return Tags(tags_p, self)
646 """A message() is represented by a 1-line summary"""
648 msg['from'] = self.get_header('from')
649 msg['tags'] = str(self.get_tags())
650 msg['date'] = date.fromtimestamp(self.get_date())
651 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
653 def format_as_text(self):
654 """ Output like notmuch show """
658 """Close and free the notmuch Message"""
659 if self._msg is not None:
660 logging.debug("Freeing the Message now")
661 nmlib.notmuch_message_destroy (self._msg)