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
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_get_version"""
27 _get_version = nmlib.notmuch_database_get_version
28 _get_version.restype = c_uint
30 """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
31 _open = nmlib.notmuch_database_open
32 _open.restype = c_void_p
34 """ notmuch_database_find_message """
35 _find_message = nmlib.notmuch_database_find_message
36 _find_message.restype = c_void_p
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
42 """ notmuch_database_create(const char *path):"""
43 _create = nmlib.notmuch_database_create
44 _create.restype = c_void_p
46 def __init__(self, path=None, create=False, mode= MODE.READ_ONLY):
47 """If *path* is *None*, we will try to read a users notmuch
48 configuration and use his default database. If *create* is `True`,
49 the database will always be created in
50 :attr:`MODE`.READ_WRITE mode.
52 :param path: Directory to open/create the database in (see
53 above for behavior if `None`)
54 :type path: `str` or `None`
55 :param create: Pass `False` to open an existing, `True` to create a new
58 :param mode: Mode to open a database in. Is always
59 :attr:`MODE`.READ_WRITE when creating a new one.
60 :type mode: :attr:`MODE`
62 :exception: :exc:`NotmuchError` in case of failure.
66 # no path specified. use a user's default database
67 if Database._std_db_path is None:
68 #the following line throws a NotmuchError if it fails
69 Database._std_db_path = self._get_user_default_db()
70 path = Database._std_db_path
77 def create(self, path):
78 """Creates a new notmuch database
80 This function is used by __init__() and usually does not need
81 to be called directly. It wraps the underlying
82 *notmuch_database_create* function and creates a new notmuch
83 database at *path*. It will always return a database in
84 :attr:`MODE`.READ_WRITE mode as creating an empty database for
85 reading only does not make a great deal of sense.
87 :param path: A directory in which we should create the database.
90 :exception: :exc:`NotmuchError` in case of any failure
91 (after printing an error message on stderr).
93 if self._db is not None:
95 message="Cannot create db, this Database() already has an open one.")
97 res = Database._create(path, MODE.READ_WRITE)
101 message="Could not create the specified database")
104 def open(self, path, mode= MODE.READ_ONLY):
105 """Opens an existing database
107 This function is used by __init__() and usually does not need
108 to be called directly. It wraps the underlying
109 *notmuch_database_open* function.
111 :param status: Open the database in read-only or read-write mode
112 :type status: :attr:`MODE`
114 :exception: Raises :exc:`NotmuchError` in case
115 of any failure (after printing an error message on stderr).
118 res = Database._open(path, mode)
122 message="Could not open the specified database")
126 """Returns the file path of an open database
128 Wraps notmuch_database_get_path"""
129 return Database._get_path(self._db)
131 def get_version(self):
132 """Returns the database format version
134 :returns: The database version as positive integer
135 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
136 the database was not intitialized.
139 raise NotmuchError(STATUS.NOT_INITIALIZED)
141 return Database._get_version (self._db)
143 def needs_upgrade(self):
144 """Does this database need to be upgraded before writing to it?
146 If this function returns TRUE then no functions that modify the
147 database (:meth:`Database.add_message`, :meth:`Database.add_tag`,
148 :meth:`Directory.set_mtime`, etc.) will work unless :meth:`upgrade`
149 is called successfully first.
151 :returns: `True` or `False`
152 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
153 the database was not intitialized.
156 raise NotmuchError(STATUS.NOT_INITIALIZED)
158 return notmuch_database_needs_upgrade(self.db)
160 def find_message(self, msgid):
161 """Returns a :class:`Message` as identified by its message ID
163 Wraps the underlying *notmuch_database_find_message* function.
165 :param msgid: The message ID
167 :returns: :class:`Message` or `None` if no message is found or if an
168 out-of-memory situation occurs.
169 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
170 the database was not intitialized.
173 raise NotmuchError(STATUS.NOT_INITIALIZED)
174 msg_p = Database._find_message(self._db, msgid)
177 return Message(msg_p, self)
179 def get_all_tags(self):
180 """Returns :class:`Tags` with a list of all tags found in the database
182 :returns: :class:`Tags`
183 :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
186 raise NotmuchError(STATUS.NOT_INITIALIZED)
188 tags_p = Database._get_all_tags (self._db)
190 raise NotmuchError(STATUS.NULL_POINTER)
191 return Tags(tags_p, self)
194 return "'Notmuch DB " + self.get_path() + "'"
197 """Close and free the notmuch database if needed"""
198 if self._db is not None:
199 logging.debug("Freeing the database now")
200 nmlib.notmuch_database_close(self._db)
202 def _get_user_default_db(self):
203 """ Reads a user's notmuch config and returns his db location
205 Throws a NotmuchError if it cannot find it"""
206 from ConfigParser import SafeConfigParser
208 config = SafeConfigParser()
209 config.read(os.path.expanduser('~/.notmuch-config'))
210 if not config.has_option('database','path'):
211 raise NotmuchError(message=
212 "No DB path specified and no user default found")
213 return config.get('database','path')
217 """Property returning a pointer to the notmuch_database_t or `None`
219 This should normally not be needed by a user."""
222 #------------------------------------------------------------------------------
224 """ Represents a search query on an opened :class:`Database`.
226 A query selects and filters a subset of messages from the notmuch
227 database we derive from.
229 Technically, it wraps the underlying *notmuch_query_t* struct.
231 .. note:: Do remember that as soon as we tear down this object,
232 all underlying derived objects such as threads,
233 messages, tags etc will be freed by the underlying library
234 as well. Accessing these objects will lead to segfaults and
235 other unexpected behavior. See above for more details.
238 SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
239 """Constants: Sort order in which to return results"""
241 """notmuch_query_create"""
242 _create = nmlib.notmuch_query_create
243 _create.restype = c_void_p
245 """notmuch_query_search_messages"""
246 _search_messages = nmlib.notmuch_query_search_messages
247 _search_messages.restype = c_void_p
249 def __init__(self, db, querystr):
251 :param db: An open database which we derive the Query from.
252 :type db: :class:`Database`
253 :param querystr: The query string for the message.
258 self.create(db, querystr)
260 def create(self, db, querystr):
261 """Creates a new query derived from a Database.
263 This function is utilized by __init__() and usually does not need to
266 :param db: Database to create the query from.
267 :type db: :class:`Database`
268 :param querystr: The query string
271 :exception: :exc:`NotmuchError`
273 * STATUS.NOT_INITIALIZED if db is not inited
274 * STATUS.NULL_POINTER if the query creation failed
278 raise NotmuchError(STATUS.NOT_INITIALIZED)
279 # create reference to parent db to keep it alive
282 # create query, return None if too little mem available
283 query_p = Query._create(db.db_p, querystr)
285 NotmuchError(STATUS.NULL_POINTER)
286 self._query = query_p
288 def set_sort(self, sort):
289 """Set the sort order future results will be delivered in
291 Wraps the underlying *notmuch_query_set_sort* function.
293 :param sort: Sort order (see :attr:`Query.SORT`)
295 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
298 if self._query is None:
299 raise NotmuchError(STATUS.NOT_INITIALIZED)
301 nmlib.notmuch_query_set_sort(self._query, sort)
303 def search_messages(self):
304 """Filter messages according to the query and return
305 :class:`Messages` in the defined sort order
307 Technically, it wraps the underlying
308 *notmuch_query_search_messages* function.
310 :returns: :class:`Messages`
311 :exception: :exc:`NotmuchError`
313 * STATUS.NOT_INITIALIZED if query is not inited
314 * STATUS.NULL_POINTER if search_messages failed
316 if self._query is None:
317 raise NotmuchError(STATUS.NOT_INITIALIZED)
319 msgs_p = Query._search_messages(self._query)
322 NotmuchError(STATUS.NULL_POINTER)
324 return Messages(msgs_p,self)
328 """Close and free the Query"""
329 if self._query is not None:
330 logging.debug("Freeing the Query now")
331 nmlib.notmuch_query_destroy (self._query)
333 #------------------------------------------------------------------------------
335 """Represents a list of notmuch tags
337 This object provides an iterator over a list of notmuch tags. Do
338 note that the underlying library only provides a one-time iterator
339 (it cannot reset the iterator to the start). Thus iterating over
340 the function will "exhaust" the list of tags, and a subsequent
341 iteration attempt will raise a :exc:`NotmuchError`
342 STATUS.NOT_INITIALIZED. Also note, that any function that uses
343 iteration (nearly all) will also exhaust the tags. So both::
345 for tag in tags: print tag
349 number_of_tags = len(tags)
353 #str() iterates over all tags to construct a space separated list
356 will "exhaust" the Tags. If you need to re-iterate over a list of
357 tags you will need to retrieve a new :class:`Tags` object.
361 _get = nmlib.notmuch_tags_get
362 _get.restype = c_char_p
364 def __init__(self, tags_p, parent=None):
366 :param tags_p: A pointer to an underlying *notmuch_tags_t*
367 structure. These are not publically exposed, so a user
368 will almost never instantiate a :class:`Tags` object
369 herself. They are usually handed back as a result,
370 e.g. in :meth:`Database.get_all_tags`. *tags_p* must be
371 valid, we will raise an :exc:`NotmuchError`
372 (STATUS.NULL_POINTER) if it is `None`.
373 :type tags_p: :class:`ctypes.c_void_p`
374 :param parent: The parent object (ie :class:`Database` or
375 :class:`Message` these tags are derived from, and saves a
376 reference to it, so we can automatically delete the db object
377 once all derived objects are dead.
378 :TODO: Make the iterator optionally work more than once by
379 cache the tags in the Python object(?)
382 NotmuchError(STATUS.NULL_POINTER)
385 #save reference to parent object so we keep it alive
386 self._parent = parent
387 logging.debug("Inited Tags derived from %s" %(repr(parent)))
390 """ Make Tags an iterator """
394 if self._tags is None:
395 raise NotmuchError(STATUS.NOT_INITIALIZED)
397 if not nmlib.notmuch_tags_valid(self._tags):
401 tag = Tags._get (self._tags)
402 nmlib.notmuch_tags_move_to_next(self._tags)
406 """len(:class:`Tags`) returns the number of contained tags
408 .. note:: As this iterates over the tags, we will not be able
409 to iterate over them again (as in retrieve them)! If
410 the tags have been exhausted already, this will raise a
411 :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
414 if self._tags is None:
415 raise NotmuchError(STATUS.NOT_INITIALIZED)
418 while nmlib.notmuch_tags_valid(self._msgs):
419 nmlib.notmuch_tags_move_to_next(self._msgs)
425 """The str() representation of Tags() is a space separated list of tags
427 .. note:: As this iterates over the tags, we will not be able
428 to iterate over them again (as in retrieve them)! If
429 the tags have been exhausted already, this will raise a
430 :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
433 return " ".join(self)
436 """Close and free the notmuch tags"""
437 if self._tags is not None:
438 logging.debug("Freeing the Tags now")
439 nmlib.notmuch_tags_destroy (self._tags)
442 #------------------------------------------------------------------------------
443 class Messages(object):
444 """Represents a list of notmuch messages
446 This object provides an iterator over a list of notmuch messages
447 (Technically, it provides a wrapper for the underlying
448 *notmuch_messages_t* structure). Do note that the underlying
449 library only provides a one-time iterator (it cannot reset the
450 iterator to the start). Thus iterating over the function will
451 "exhaust" the list of messages, and a subsequent iteration attempt
452 will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
453 note, that any function that uses iteration will also
454 exhaust the messages. So both::
456 for msg in msgs: print msg
460 number_of_msgs = len(msgs)
462 will "exhaust" the Messages. If you need to re-iterate over a list of
463 messages you will need to retrieve a new :class:`Messages` object.
465 Things are not as bad as it seems though, you can store and reuse
466 the single Message objects as often as you want as long as you
467 keep the parent Messages object around. (Recall that due to
468 hierarchical memory allocation, all derived Message objects will
469 be invalid when we delete the parent Messages() object, even if it
470 was already "exhausted".) So this works::
473 msgs = Query(db,'').search_messages() #get a Messages() object
478 # msgs is "exhausted" now and even len(msgs) will raise an exception.
479 # However it will be kept around until all retrieved Message() objects are
480 # also deleted. If you did e.g. an explicit del(msgs) here, the
481 # following lines would fail.
483 # You can reiterate over *msglist* however as often as you want.
484 # It is simply a list with Message objects.
486 print (msglist[0].get_filename())
487 print (msglist[1].get_filename())
488 print (msglist[0].get_message_id())
492 _get = nmlib.notmuch_messages_get
493 _get.restype = c_void_p
495 _collect_tags = nmlib.notmuch_messages_collect_tags
496 _collect_tags.restype = c_void_p
498 def __init__(self, msgs_p, parent=None):
500 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
501 structure. These are not publically exposed, so a user
502 will almost never instantiate a :class:`Messages` object
503 herself. They are usually handed back as a result,
504 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
505 valid, we will raise an :exc:`NotmuchError`
506 (STATUS.NULL_POINTER) if it is `None`.
507 :type msgs_p: :class:`ctypes.c_void_p`
508 :param parent: The parent object
509 (ie :class:`Query`) these tags are derived from. It saves
510 a reference to it, so we can automatically delete the db
511 object once all derived objects are dead.
512 :TODO: Make the iterator work more than once and cache the tags in
513 the Python object.(?)
516 NotmuchError(STATUS.NULL_POINTER)
519 #store parent, so we keep them alive as long as self is alive
520 self._parent = parent
521 logging.debug("Inited Messages derived from %s" %(str(parent)))
523 def collect_tags(self):
524 """Return the unique :class:`Tags` in the contained messages
526 :returns: :class:`Tags`
527 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
529 .. note:: :meth:`collect_tags` will iterate over the messages and
530 therefore will not allow further iterations.
532 if self._msgs is None:
533 raise NotmuchError(STATUS.NOT_INITIALIZED)
535 # collect all tags (returns NULL on error)
536 tags_p = Messages._collect_tags (self._msgs)
537 #reset _msgs as we iterated over it and can do so only once
541 raise NotmuchError(STATUS.NULL_POINTER)
542 return Tags(tags_p, self)
545 """ Make Messages an iterator """
549 if self._msgs is None:
550 raise NotmuchError(STATUS.NOT_INITIALIZED)
552 if not nmlib.notmuch_messages_valid(self._msgs):
556 msg = Message(Messages._get (self._msgs), self)
557 nmlib.notmuch_messages_move_to_next(self._msgs)
561 """len(:class:`Messages`) returns the number of contained messages
563 .. note:: As this iterates over the messages, we will not be able to
564 iterate over them again (as in retrieve them)!
566 if self._msgs is None:
567 raise NotmuchError(STATUS.NOT_INITIALIZED)
570 while nmlib.notmuch_messages_valid(self._msgs):
571 nmlib.notmuch_messages_move_to_next(self._msgs)
579 """Close and free the notmuch Messages"""
580 if self._msgs is not None:
581 logging.debug("Freeing the Messages now")
582 nmlib.notmuch_messages_destroy (self._msgs)
585 #------------------------------------------------------------------------------
586 class Message(object):
587 """Represents a single Email message
589 Technically, this wraps the underlying *notmuch_message_t* structure.
592 """notmuch_message_get_filename (notmuch_message_t *message)"""
593 _get_filename = nmlib.notmuch_message_get_filename
594 _get_filename.restype = c_char_p
595 """notmuch_message_get_message_id (notmuch_message_t *message)"""
596 _get_message_id = nmlib.notmuch_message_get_message_id
597 _get_message_id.restype = c_char_p
599 """notmuch_message_get_tags (notmuch_message_t *message)"""
600 _get_tags = nmlib.notmuch_message_get_tags
601 _get_tags.restype = c_void_p
603 _get_date = nmlib.notmuch_message_get_date
604 _get_date.restype = c_uint64
606 _get_header = nmlib.notmuch_message_get_header
607 _get_header.restype = c_char_p
609 def __init__(self, msg_p, parent=None):
611 :param msg_p: A pointer to an internal notmuch_message_t
612 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
614 :param parent: A 'parent' object is passed which this message is
615 derived from. We save a reference to it, so we can
616 automatically delete the parent object once all derived
620 NotmuchError(STATUS.NULL_POINTER)
622 #keep reference to parent, so we keep it alive
623 self._parent = parent
624 logging.debug("Inited Message derived from %s" %(str(parent)))
627 def get_message_id(self):
628 """Return the message ID
630 :returns: String with a message ID
631 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
634 if self._msg is None:
635 raise NotmuchError(STATUS.NOT_INITIALIZED)
636 return Message._get_message_id(self._msg)
639 """Returns time_t of the message date
641 For the original textual representation of the Date header from the
642 message call notmuch_message_get_header() with a header value of
645 :returns: a time_t timestamp
647 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
650 if self._msg is None:
651 raise NotmuchError(STATUS.NOT_INITIALIZED)
652 return Message._get_date(self._msg)
654 def get_header(self, header):
655 """Returns a message header
657 This returns any message header that is stored in the notmuch database.
658 This is only a selected subset of headers, which is currently:
660 TODO: add stored headers
662 :param header: The name of the header to be retrieved.
663 It is not case-sensitive (TODO: confirm).
665 :returns: The header value as string
666 :exception: :exc:`NotmuchError`
668 * STATUS.NOT_INITIALIZED if the message
670 * STATUS.NULL_POINTER, if no header was found
672 if self._msg is None:
673 raise NotmuchError(STATUS.NOT_INITIALIZED)
675 #Returns NULL if any error occurs.
676 header = Message._get_header (self._msg, header)
678 raise NotmuchError(STATUS.NULL_POINTER)
681 def get_filename(self):
682 """Return the file path of the message file
684 :returns: Absolute file path & name of the message file
685 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
688 if self._msg is None:
689 raise NotmuchError(STATUS.NOT_INITIALIZED)
690 return Message._get_filename(self._msg)
693 """ Return the message tags
695 :returns: Message tags
696 :rtype: :class:`Tags`
697 :exception: :exc:`NotmuchError`
699 * STATUS.NOT_INITIALIZED if the message
701 * STATUS.NULL_POINTER, on error
703 if self._msg is None:
704 raise NotmuchError(STATUS.NOT_INITIALIZED)
706 tags_p = Message._get_tags(self._msg)
708 raise NotmuchError(STATUS.NULL_POINTER)
709 return Tags(tags_p, self)
711 def add_tag(self, tag):
712 """Add a tag to the given message
714 Adds a tag to the current message. The maximal tag length is defined in
715 the notmuch library and is currently 200 bytes.
717 :param tag: String with a 'tag' to be added.
718 :returns: STATUS.SUCCESS if the tag was successfully added.
719 Raises an exception otherwise.
720 :exception: :exc:`NotmuchError`. They have the following meaning:
723 The 'tag' argument is NULL
726 The length of 'tag' is too long
727 (exceeds Message.NOTMUCH_TAG_MAX)
729 STATUS.READ_ONLY_DATABASE
730 Database was opened in read-only mode so message cannot be
733 STATUS.NOT_INITIALIZED
734 The message has not been initialized.
736 if self._msg is None:
737 raise NotmuchError(STATUS.NOT_INITIALIZED)
739 status = nmlib.notmuch_message_add_tag (self._msg, tag)
741 if STATUS.SUCCESS == status:
745 raise NotmuchError(status)
747 def remove_tag(self, tag):
748 """Removes a tag from the given message
750 If the message has no such tag, this is a non-operation and
751 will report success anyway.
753 :param tag: String with a 'tag' to be removed.
754 :returns: STATUS.SUCCESS if the tag was successfully removed or if
755 the message had no such tag.
756 Raises an exception otherwise.
757 :exception: :exc:`NotmuchError`. They have the following meaning:
760 The 'tag' argument is NULL
761 NOTMUCH_STATUS_TAG_TOO_LONG
762 The length of 'tag' is too long
763 (exceeds NOTMUCH_TAG_MAX)
764 NOTMUCH_STATUS_READ_ONLY_DATABASE
765 Database was opened in read-only mode so message cannot
767 STATUS.NOT_INITIALIZED
768 The message has not been initialized.
771 if self._msg is None:
772 raise NotmuchError(STATUS.NOT_INITIALIZED)
774 status = nmlib.notmuch_message_remove_tag(self._msg, tag)
776 if STATUS.SUCCESS == status:
780 raise NotmuchError(status)
783 """A message() is represented by a 1-line summary"""
785 msg['from'] = self.get_header('from')
786 msg['tags'] = str(self.get_tags())
787 msg['date'] = date.fromtimestamp(self.get_date())
788 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
790 def format_as_text(self):
791 """Output like notmuch show (Not implemented)"""
795 """Close and free the notmuch Message"""
796 if self._msg is not None:
797 logging.debug("Freeing the Message now")
798 nmlib.notmuch_message_destroy (self._msg)