summaryrefslogtreecommitdiff
path: root/bindings/python/notmuch
diff options
context:
space:
mode:
authorCarl Worth <cworth@cworth.org>2010-04-21 17:23:49 -0700
committerCarl Worth <cworth@cworth.org>2010-04-21 17:29:34 -0700
commit3b558de7811a765c3295a58bd53e2156eca0e32e (patch)
tree3367ab027520d8049b92c16bb89aefec52d80805 /bindings/python/notmuch
parent634b4fbe233ee9007d190327df11779fe2113fb8 (diff)
Move everything down into a bindings/python directory.
In preparation for merging the python bindings into the notmuch repository.
Diffstat (limited to 'bindings/python/notmuch')
-rw-r--r--bindings/python/notmuch/__init__.py61
-rw-r--r--bindings/python/notmuch/database.py841
-rw-r--r--bindings/python/notmuch/globals.py80
-rw-r--r--bindings/python/notmuch/message.py777
-rw-r--r--bindings/python/notmuch/tag.py126
-rw-r--r--bindings/python/notmuch/thread.py370
6 files changed, 2255 insertions, 0 deletions
diff --git a/bindings/python/notmuch/__init__.py b/bindings/python/notmuch/__init__.py
new file mode 100644
index 0000000..56a4f2a
--- /dev/null
+++ b/bindings/python/notmuch/__init__.py
@@ -0,0 +1,61 @@
+"""The :mod:`notmuch` module provides most of the functionality that a user is likely to need.
+
+.. note:: The underlying notmuch library is build on a hierarchical
+ memory allocator called talloc. All objects derive from a
+ top-level :class:`Database` object.
+
+ This means that as soon as an object is deleted, all underlying
+ derived objects such as Queries, Messages, Message, and Tags will
+ be freed by the underlying library as well. Accessing these
+ objects will then lead to segfaults and other unexpected behavior.
+
+ We implement reference counting, so that parent objects can be
+ automatically freed when they are not needed anymore. For
+ example::
+
+ db = Database('path',create=True)
+ msgs = Query(db,'from:myself').search_messages()
+
+ This returns a :class:`Messages` which internally contains a
+ reference to its parent :class:`Query` object. Otherwise the
+ Query() would be immediately freed, taking our *msgs* down with
+ it.
+
+ In this case, the above Query() object will be automatically freed
+ whenever we delete all derived objects, ie in our case:
+ `del(msgs)` would also delete the parent Query. It would not
+ delete the parent Database() though, as that is still referenced
+ from the variable *db* in which it is stored.
+
+ Pretty much the same is valid for all other objects in the
+ hierarchy, such as :class:`Query`, :class:`Messages`,
+ :class:`Message`, and :class:`Tags`.
+
+"""
+
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch. If not, see <http://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+"""
+from database import Database, Query
+from message import Messages, Message
+from thread import Threads, Thread
+from tag import Tags
+from notmuch.globals import nmlib, STATUS, NotmuchError
+__LICENSE__="GPL v3+"
+__VERSION__='0.2.2'
+__AUTHOR__ ='Sebastian Spaeth <Sebastian@SSpaeth.de>'
diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py
new file mode 100644
index 0000000..f141c03
--- /dev/null
+++ b/bindings/python/notmuch/database.py
@@ -0,0 +1,841 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch. If not, see <http://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+"""
+
+import os
+from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref
+from notmuch.globals import nmlib, STATUS, NotmuchError, Enum
+from notmuch.thread import Threads
+from notmuch.message import Messages, Message
+from notmuch.tag import Tags
+
+class Database(object):
+ """Represents a notmuch database (wraps notmuch_database_t)
+
+ .. note:: Do remember that as soon as we tear down this object,
+ all underlying derived objects such as queries, threads,
+ messages, tags etc will be freed by the underlying library
+ as well. Accessing these objects will lead to segfaults and
+ other unexpected behavior. See above for more details.
+ """
+ _std_db_path = None
+ """Class attribute to cache user's default database"""
+
+ MODE = Enum(['READ_ONLY','READ_WRITE'])
+ """Constants: Mode in which to open the database"""
+
+ """notmuch_database_get_directory"""
+ _get_directory = nmlib.notmuch_database_get_directory
+ _get_directory.restype = c_void_p
+
+ """notmuch_database_get_path"""
+ _get_path = nmlib.notmuch_database_get_path
+ _get_path.restype = c_char_p
+
+ """notmuch_database_get_version"""
+ _get_version = nmlib.notmuch_database_get_version
+ _get_version.restype = c_uint
+
+ """notmuch_database_open"""
+ _open = nmlib.notmuch_database_open
+ _open.restype = c_void_p
+
+ """notmuch_database_upgrade"""
+ _upgrade = nmlib.notmuch_database_upgrade
+ _upgrade.argtypes = [c_void_p, c_void_p, c_void_p]
+
+ """ notmuch_database_find_message"""
+ _find_message = nmlib.notmuch_database_find_message
+ _find_message.restype = c_void_p
+
+ """notmuch_database_get_all_tags"""
+ _get_all_tags = nmlib.notmuch_database_get_all_tags
+ _get_all_tags.restype = c_void_p
+
+ """notmuch_database_create"""
+ _create = nmlib.notmuch_database_create
+ _create.restype = c_void_p
+
+ def __init__(self, path=None, create=False, mode= 0):
+ """If *path* is `None`, we will try to read a users notmuch
+ configuration and use his configured database. The location of the
+ configuration file can be specified through the environment variable
+ *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
+
+ If *create* is `True`, the database will always be created in
+ :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
+
+ :param path: Directory to open/create the database in (see
+ above for behavior if `None`)
+ :type path: `str` or `None`
+ :param create: Pass `False` to open an existing, `True` to create a new
+ database.
+ :type create: bool
+ :param mode: Mode to open a database in. Is always
+ :attr:`MODE`.READ_WRITE when creating a new one.
+ :type mode: :attr:`MODE`
+ :returns: Nothing
+ :exception: :exc:`NotmuchError` in case of failure.
+ """
+ self._db = None
+ if path is None:
+ # no path specified. use a user's default database
+ if Database._std_db_path is None:
+ #the following line throws a NotmuchError if it fails
+ Database._std_db_path = self._get_user_default_db()
+ path = Database._std_db_path
+
+ if create == False:
+ self.open(path, mode)
+ else:
+ self.create(path)
+
+ def _verify_initialized_db(self):
+ """Raises a NotmuchError in case self._db is still None"""
+ if self._db is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ def create(self, path):
+ """Creates a new notmuch database
+
+ This function is used by __init__() and usually does not need
+ to be called directly. It wraps the underlying
+ *notmuch_database_create* function and creates a new notmuch
+ database at *path*. It will always return a database in :attr:`MODE`
+ .READ_WRITE mode as creating an empty database for
+ reading only does not make a great deal of sense.
+
+ :param path: A directory in which we should create the database.
+ :type path: str
+ :returns: Nothing
+ :exception: :exc:`NotmuchError` in case of any failure
+ (after printing an error message on stderr).
+ """
+ if self._db is not None:
+ raise NotmuchError(
+ message="Cannot create db, this Database() already has an open one.")
+
+ res = Database._create(path, Database.MODE.READ_WRITE)
+
+ if res is None:
+ raise NotmuchError(
+ message="Could not create the specified database")
+ self._db = res
+
+ def open(self, path, mode= 0):
+ """Opens an existing database
+
+ This function is used by __init__() and usually does not need
+ to be called directly. It wraps the underlying
+ *notmuch_database_open* function.
+
+ :param status: Open the database in read-only or read-write mode
+ :type status: :attr:`MODE`
+ :returns: Nothing
+ :exception: Raises :exc:`NotmuchError` in case
+ of any failure (after printing an error message on stderr).
+ """
+
+ res = Database._open(path, mode)
+
+ if res is None:
+ raise NotmuchError(
+ message="Could not open the specified database")
+ self._db = res
+
+ def get_path(self):
+ """Returns the file path of an open database
+
+ Wraps *notmuch_database_get_path*."""
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ return Database._get_path(self._db)
+
+ def get_version(self):
+ """Returns the database format version
+
+ :returns: The database version as positive integer
+ :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
+ the database was not intitialized.
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ return Database._get_version (self._db)
+
+ def needs_upgrade(self):
+ """Does this database need to be upgraded before writing to it?
+
+ If this function returns `True` then no functions that modify the
+ database (:meth:`add_message`,
+ :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
+ etc.) will work unless :meth:`upgrade` is called successfully first.
+
+ :returns: `True` or `False`
+ :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
+ the database was not intitialized.
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ return notmuch_database_needs_upgrade(self._db)
+
+ def upgrade(self):
+ """Upgrades the current database
+
+ After opening a database in read-write mode, the client should
+ check if an upgrade is needed (notmuch_database_needs_upgrade) and
+ if so, upgrade with this function before making any modifications.
+
+ NOT IMPLEMENTED: The optional progress_notify callback can be
+ used by the caller to provide progress indication to the
+ user. If non-NULL it will be called periodically with
+ 'progress' as a floating-point value in the range of [0.0..1.0]
+ indicating the progress made so far in the upgrade process.
+
+ :TODO: catch exceptions, document return values and etc...
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ status = Database._upgrade (self._db, None, None)
+ #TODO: catch exceptions, document return values and etc
+ return status
+
+ def get_directory(self, path):
+ """Returns a :class:`Directory` of path,
+ (creating it if it does not exist(?))
+
+ .. warning:: This call needs a writeable database in
+ Database.MODE.READ_WRITE mode. The underlying library will exit the
+ program if this method is used on a read-only database!
+
+ :param path: A str containing the path relative to the path of database
+ (see :meth:`get_path`), or else should be an absolute path
+ with initial components that match the path of 'database'.
+ :returns: :class:`Directory` or raises an exception.
+ :exception: :exc:`NotmuchError`
+
+ STATUS.NOT_INITIALIZED
+ If the database was not intitialized.
+
+ STATUS.FILE_ERROR
+ If path is not relative database or absolute with initial
+ components same as database.
+
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ # sanity checking if path is valid, and make path absolute
+ if path[0] == os.sep:
+ # we got an absolute path
+ if not path.startswith(self.get_path()):
+ # but its initial components are not equal to the db path
+ raise NotmuchError(STATUS.FILE_ERROR,
+ message="Database().get_directory() called with a wrong absolute path.")
+ abs_dirpath = path
+ else:
+ #we got a relative path, make it absolute
+ abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path))
+
+ dir_p = Database._get_directory(self._db, path);
+
+ # return the Directory, init it with the absolute path
+ return Directory(abs_dirpath, dir_p, self)
+
+ def add_message(self, filename):
+ """Adds a new message to the database
+
+ `filename` should be a path relative to the path of the open
+ database (see :meth:`get_path`), or else should be an absolute
+ filename with initial components that match the path of the
+ database.
+
+ The file should be a single mail message (not a multi-message mbox)
+ that is expected to remain at its current location, since the
+ notmuch database will reference the filename, and will not copy the
+ entire contents of the file.
+
+ :returns: On success, we return
+
+ 1) a :class:`Message` object that can be used for things
+ such as adding tags to the just-added message.
+ 2) one of the following STATUS values:
+
+ STATUS.SUCCESS
+ Message successfully added to database.
+ STATUS.DUPLICATE_MESSAGE_ID
+ Message has the same message ID as another message already
+ in the database. The new filename was successfully added
+ to the message in the database.
+
+ :rtype: 2-tuple(:class:`Message`, STATUS)
+
+ :exception: Raises a :exc:`NotmuchError` with the following meaning.
+ If such an exception occurs, nothing was added to the database.
+
+ STATUS.FILE_ERROR
+ An error occurred trying to open the file, (such as
+ permission denied, or file not found, etc.).
+ STATUS.FILE_NOT_EMAIL
+ The contents of filename don't look like an email message.
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so no message can
+ be added.
+ STATUS.NOT_INITIALIZED
+ The database has not been initialized.
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ msg_p = c_void_p()
+ status = nmlib.notmuch_database_add_message(self._db,
+ filename,
+ byref(msg_p))
+
+ if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
+ raise NotmuchError(status)
+
+ #construct Message() and return
+ msg = Message(msg_p, self)
+ return (msg, status)
+
+ def remove_message(self, filename):
+ """Removes a message from the given notmuch database
+
+ Note that only this particular filename association is removed from
+ the database. If the same message (as determined by the message ID)
+ is still available via other filenames, then the message will
+ persist in the database for those filenames. When the last filename
+ is removed for a particular message, the database content for that
+ message will be entirely removed.
+
+ :returns: A STATUS value with the following meaning:
+
+ STATUS.SUCCESS
+ The last filename was removed and the message was removed
+ from the database.
+ STATUS.DUPLICATE_MESSAGE_ID
+ This filename was removed but the message persists in the
+ database with at least one other filename.
+
+ :exception: Raises a :exc:`NotmuchError` with the following meaning.
+ If such an exception occurs, nothing was removed from the database.
+
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so no message can be
+ removed.
+ STATUS.NOT_INITIALIZED
+ The database has not been initialized.
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ return nmlib.notmuch_database_remove_message(self._db,
+ filename)
+
+ def find_message(self, msgid):
+ """Returns a :class:`Message` as identified by its message ID
+
+ Wraps the underlying *notmuch_database_find_message* function.
+
+ :param msgid: The message ID
+ :type msgid: string
+ :returns: :class:`Message` or `None` if no message is found or if an
+ out-of-memory situation occurs.
+ :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
+ the database was not intitialized.
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ msg_p = Database._find_message(self._db, msgid)
+ if msg_p is None:
+ return None
+ return Message(msg_p, self)
+
+ def get_all_tags(self):
+ """Returns :class:`Tags` with a list of all tags found in the database
+
+ :returns: :class:`Tags`
+ :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ tags_p = Database._get_all_tags (self._db)
+ if tags_p == None:
+ raise NotmuchError(STATUS.NULL_POINTER)
+ return Tags(tags_p, self)
+
+ def create_query(self, querystring):
+ """Returns a :class:`Query` derived from this database
+
+ This is a shorthand method for doing::
+
+ # short version
+ # Automatically frees the Database() when 'q' is deleted
+
+ q = Database(dbpath).create_query('from:"Biene Maja"')
+
+ # long version, which is functionally equivalent but will keep the
+ # Database in the 'db' variable around after we delete 'q':
+
+ db = Database(dbpath)
+ q = Query(db,'from:"Biene Maja"')
+
+ This function is a python extension and not in the underlying C API.
+ """
+ # Raise a NotmuchError if not initialized
+ self._verify_initialized_db()
+
+ return Query(self, querystring)
+
+ def __repr__(self):
+ return "'Notmuch DB " + self.get_path() + "'"
+
+ def __del__(self):
+ """Close and free the notmuch database if needed"""
+ if self._db is not None:
+ nmlib.notmuch_database_close(self._db)
+
+ def _get_user_default_db(self):
+ """ Reads a user's notmuch config and returns his db location
+
+ Throws a NotmuchError if it cannot find it"""
+ from ConfigParser import SafeConfigParser
+ config = SafeConfigParser()
+ conf_f = os.getenv('NOTMUCH_CONFIG',
+ os.path.expanduser('~/.notmuch-config'))
+ config.read(conf_f)
+ if not config.has_option('database','path'):
+ raise NotmuchError(message=
+ "No DB path specified and no user default found")
+ return config.get('database','path')
+
+ @property
+ def db_p(self):
+ """Property returning a pointer to `notmuch_database_t` or `None`
+
+ This should normally not be needed by a user (and is not yet
+ guaranteed to remain stable in future versions).
+ """
+ return self._db
+
+#------------------------------------------------------------------------------
+class Query(object):
+ """Represents a search query on an opened :class:`Database`.
+
+ A query selects and filters a subset of messages from the notmuch
+ database we derive from.
+
+ Query() provides an instance attribute :attr:`sort`, which
+ contains the sort order (if specified via :meth:`set_sort`) or
+ `None`.
+
+ Technically, it wraps the underlying *notmuch_query_t* struct.
+
+ .. note:: Do remember that as soon as we tear down this object,
+ all underlying derived objects such as threads,
+ messages, tags etc will be freed by the underlying library
+ as well. Accessing these objects will lead to segfaults and
+ other unexpected behavior. See above for more details.
+ """
+ # constants
+ SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
+ """Constants: Sort order in which to return results"""
+
+ """notmuch_query_create"""
+ _create = nmlib.notmuch_query_create
+ _create.restype = c_void_p
+
+ """notmuch_query_search_threads"""
+ _search_threads = nmlib.notmuch_query_search_threads
+ _search_threads.restype = c_void_p
+
+ """notmuch_query_search_messages"""
+ _search_messages = nmlib.notmuch_query_search_messages
+ _search_messages.restype = c_void_p
+
+
+ """notmuch_query_count_messages"""
+ _count_messages = nmlib.notmuch_query_count_messages
+ _count_messages.restype = c_uint
+
+ def __init__(self, db, querystr):
+ """
+ :param db: An open database which we derive the Query from.
+ :type db: :class:`Database`
+ :param querystr: The query string for the message.
+ :type querystr: str
+ """
+ self._db = None
+ self._query = None
+ self.sort = None
+ self.create(db, querystr)
+
+ def create(self, db, querystr):
+ """Creates a new query derived from a Database
+
+ This function is utilized by __init__() and usually does not need to
+ be called directly.
+
+ :param db: Database to create the query from.
+ :type db: :class:`Database`
+ :param querystr: The query string
+ :type querystr: str
+ :returns: Nothing
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if db is not inited
+ * STATUS.NULL_POINTER if the query creation failed
+ (too little memory)
+ """
+ if db.db_p is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ # create reference to parent db to keep it alive
+ self._db = db
+
+ # create query, return None if too little mem available
+ query_p = Query._create(db.db_p, querystr)
+ if query_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+ self._query = query_p
+
+ def set_sort(self, sort):
+ """Set the sort order future results will be delivered in
+
+ Wraps the underlying *notmuch_query_set_sort* function.
+
+ :param sort: Sort order (see :attr:`Query.SORT`)
+ :returns: Nothing
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
+ been initialized.
+ """
+ if self._query is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ self.sort = sort
+ nmlib.notmuch_query_set_sort(self._query, sort)
+
+ def search_threads(self):
+ """Execute a query for threads
+
+ Execute a query for threads, returning a :class:`Threads` iterator.
+ The returned threads are owned by the query and as such, will only be
+ valid until the Query is deleted.
+
+ The method sets :attr:`Message.FLAG`\.MATCH for those messages that
+ match the query. The method :meth:`Message.get_flag` allows us
+ to get the value of this flag.
+
+ Technically, it wraps the underlying
+ *notmuch_query_search_threads* function.
+
+ :returns: :class:`Threads`
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if query is not inited
+ * STATUS.NULL_POINTER if search_threads failed
+ """
+ if self._query is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ threads_p = Query._search_threads(self._query)
+
+ if threads_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+
+ return Threads(threads_p,self)
+
+ def search_messages(self):
+ """Filter messages according to the query and return
+ :class:`Messages` in the defined sort order
+
+ Technically, it wraps the underlying
+ *notmuch_query_search_messages* function.
+
+ :returns: :class:`Messages`
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if query is not inited
+ * STATUS.NULL_POINTER if search_messages failed
+ """
+ if self._query is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ msgs_p = Query._search_messages(self._query)
+
+ if msgs_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+
+ return Messages(msgs_p,self)
+
+ def count_messages(self):
+ """Estimate the number of messages matching the query
+
+ This function performs a search and returns Xapian's best
+ guess as to the number of matching messages. It is much faster
+ than performing :meth:`search_messages` and counting the
+ result with `len()` (although it always returned the same
+ result in my tests). Technically, it wraps the underlying
+ *notmuch_query_count_messages* function.
+
+ :returns: :class:`Messages`
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if query is not inited
+ """
+ if self._query is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ return Query._count_messages(self._query)
+
+ def __del__(self):
+ """Close and free the Query"""
+ if self._query is not None:
+ nmlib.notmuch_query_destroy (self._query)
+
+
+#------------------------------------------------------------------------------
+class Directory(object):
+ """Represents a directory entry in the notmuch directory
+
+ Modifying attributes of this object will modify the
+ database, not the real directory attributes.
+
+ The Directory object is usually derived from another object
+ e.g. via :meth:`Database.get_directory`, and will automatically be
+ become invalid whenever that parent is deleted. You should
+ therefore initialized this object handing it a reference to the
+ parent, preventing the parent from automatically being garbage
+ collected.
+ """
+
+ """notmuch_directory_get_mtime"""
+ _get_mtime = nmlib.notmuch_directory_get_mtime
+ _get_mtime.restype = c_long
+
+ """notmuch_directory_set_mtime"""
+ _set_mtime = nmlib.notmuch_directory_set_mtime
+ _set_mtime.argtypes = [c_char_p, c_long]
+
+ """notmuch_directory_get_child_files"""
+ _get_child_files = nmlib.notmuch_directory_get_child_files
+ _get_child_files.restype = c_void_p
+
+ """notmuch_directory_get_child_directories"""
+ _get_child_directories = nmlib.notmuch_directory_get_child_directories
+ _get_child_directories.restype = c_void_p
+
+ def _verify_dir_initialized(self):
+ """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None"""
+ if self._dir_p is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ def __init__(self, path, dir_p, parent):
+ """
+ :param path: The absolute path of the directory object.
+ :param dir_p: The pointer to an internal notmuch_directory_t object.
+ :param parent: The object this Directory is derived from
+ (usually a :class:`Database`). We do not directly use
+ this, but store a reference to it as long as
+ this Directory object lives. This keeps the
+ parent object alive.
+ """
+ self._path = path
+ self._dir_p = dir_p
+ self._parent = parent
+
+
+ def set_mtime (self, mtime):
+ """Sets the mtime value of this directory in the database
+
+ The intention is for the caller to use the mtime to allow efficient
+ identification of new messages to be added to the database. The
+ recommended usage is as follows:
+
+ * Read the mtime of a directory from the filesystem
+
+ * Call :meth:`Database.add_message` for all mail files in
+ the directory
+
+ * Call notmuch_directory_set_mtime with the mtime read from the
+ filesystem. Then, when wanting to check for updates to the
+ directory in the future, the client can call :meth:`get_mtime`
+ and know that it only needs to add files if the mtime of the
+ directory and files are newer than the stored timestamp.
+
+ .. note:: :meth:`get_mtime` function does not allow the caller
+ to distinguish a timestamp of 0 from a non-existent
+ timestamp. So don't store a timestamp of 0 unless you are
+ comfortable with that.
+
+ :param mtime: A (time_t) timestamp
+ :returns: Nothing on success, raising an exception on failure.
+ :exception: :exc:`NotmuchError`:
+
+ STATUS.XAPIAN_EXCEPTION
+ A Xapian exception occurred, mtime not stored.
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so directory
+ mtime cannot be modified.
+ STATUS.NOT_INITIALIZED
+ The directory has not been initialized
+ """
+ #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
+ self._verify_dir_initialized()
+
+ #TODO: make sure, we convert the mtime parameter to a 'c_long'
+ status = Directory._set_mtime(self._dir_p, mtime)
+
+ #return on success
+ if status == STATUS.SUCCESS:
+ return
+ #fail with Exception otherwise
+ raise NotmuchError(status)
+
+ def get_mtime (self):
+ """Gets the mtime value of this directory in the database
+
+ Retrieves a previously stored mtime for this directory.
+
+ :param mtime: A (time_t) timestamp
+ :returns: Nothing on success, raising an exception on failure.
+ :exception: :exc:`NotmuchError`:
+
+ STATUS.NOT_INITIALIZED
+ The directory has not been initialized
+ """
+ #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None
+ self._verify_dir_initialized()
+
+ return Directory._get_mtime (self._dir_p)
+
+ # Make mtime attribute a property of Directory()
+ mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
+ and setting of the Directory *mtime* (read-write)
+
+ See :meth:`get_mtime` and :meth:`set_mtime` for usage and
+ possible exceptions.""")
+
+ def get_child_files(self):
+ """Gets a Filenames iterator listing all the filenames of
+ messages in the database within the given directory.
+
+ The returned filenames will be the basename-entries only (not
+ complete paths.
+ """
+ #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
+ self._verify_dir_initialized()
+
+ files_p = Directory._get_child_files(self._dir_p)
+ return Filenames(files_p, self)
+
+ def get_child_directories(self):
+ """Gets a :class:`Filenames` iterator listing all the filenames of
+ sub-directories in the database within the given directory
+
+ The returned filenames will be the basename-entries only (not
+ complete paths.
+ """
+ #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
+ self._verify_dir_initialized()
+
+ files_p = Directory._get_child_directories(self._dir_p)
+ return Filenames(files_p, self)
+
+ @property
+ def path(self):
+ """Returns the absolute path of this Directory (read-only)"""
+ return self._path
+
+ def __repr__(self):
+ """Object representation"""
+ return "<notmuch Directory object '%s'>" % self._path
+
+ def __del__(self):
+ """Close and free the Directory"""
+ if self._dir_p is not None:
+ nmlib.notmuch_directory_destroy(self._dir_p)
+
+#------------------------------------------------------------------------------
+class Filenames(object):
+ """An iterator over File- or Directory names that are stored in the database
+ """
+
+ #notmuch_filenames_get
+ _get = nmlib.notmuch_filenames_get
+ _get.restype = c_char_p
+
+ def __init__(self, files_p, parent):
+ """
+ :param files_p: The pointer to an internal notmuch_filenames_t object.
+ :param parent: The object this Directory is derived from
+ (usually a Directory()). We do not directly use
+ this, but store a reference to it as long as
+ this Directory object lives. This keeps the
+ parent object alive.
+ """
+ self._files_p = files_p
+ self._parent = parent
+
+ def __iter__(self):
+ """ Make Filenames an iterator """
+ return self
+
+ def next(self):
+ if self._files_p is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ if not nmlib.notmuch_filenames_valid(self._files_p):
+ self._files_p = None
+ raise StopIteration
+
+ file = Filenames._get (self._files_p)
+ nmlib.notmuch_filenames_move_to_next(self._files_p)
+ return file
+
+ def __len__(self):
+ """len(:class:`Filenames`) returns the number of contained files
+
+ .. note:: As this iterates over the files, we will not be able to
+ iterate over them again! So this will fail::
+
+ #THIS FAILS
+ files = Database().get_directory('').get_child_files()
+ if len(files) > 0: #this 'exhausts' msgs
+ # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
+ for file in files: print file
+ """
+ if self._files_p is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ i=0
+ while nmlib.notmuch_filenames_valid(self._files_p):
+ nmlib.notmuch_filenames_move_to_next(self._files_p)
+ i += 1
+ self._files_p = None
+ return i
+
+ def __del__(self):
+ """Close and free Filenames"""
+ if self._files_p is not None:
+ nmlib.notmuch_filenames_destroy(self._files_p)
diff --git a/bindings/python/notmuch/globals.py b/bindings/python/notmuch/globals.py
new file mode 100644
index 0000000..8b0d8d0
--- /dev/null
+++ b/bindings/python/notmuch/globals.py
@@ -0,0 +1,80 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch. If not, see <http://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+"""
+
+from ctypes import CDLL, c_char_p, c_int
+from ctypes.util import find_library
+
+#-----------------------------------------------------------------------------
+#package-global instance of the notmuch library
+try:
+ nmlib = CDLL("libnotmuch.so.1")
+except:
+ raise ImportError("Could not find shared 'notmuch' library.")
+
+#-----------------------------------------------------------------------------
+class Enum(object):
+ """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc..."""
+ def __init__(self, names):
+ for number, name in enumerate(names):
+ setattr(self, name, number)
+
+#-----------------------------------------------------------------------------
+class Status(Enum):
+ """Enum with a string representation of a notmuch_status_t value."""
+ __name__="foo"
+ _status2str = nmlib.notmuch_status_to_string
+ _status2str.restype = c_char_p
+ _status2str.argtypes = [c_int]
+
+ def __init__(self, statuslist):
+ """It is initialized with a list of strings that are available as
+ Status().string1 - Status().stringn attributes.
+ """
+ super(Status, self).__init__(statuslist)
+
+ @classmethod
+ def status2str(self, status):
+ """Get a string representation of a notmuch_status_t value."""
+ # define strings for custom error messages
+ if status == STATUS.NOT_INITIALIZED:
+ return "Operation on uninitialized object impossible."
+ return str(Status._status2str(status))
+
+STATUS = Status(['SUCCESS',
+ 'OUT_OF_MEMORY',
+ 'READ_ONLY_DATABASE',
+ 'XAPIAN_EXCEPTION',
+ 'FILE_ERROR',
+ 'FILE_NOT_EMAIL',
+ 'DUPLICATE_MESSAGE_ID',
+ 'NULL_POINTER',
+ 'TAG_TOO_LONG',
+ 'UNBALANCED_FREEZE_THAW',
+ 'NOT_INITIALIZED'])
+
+
+class NotmuchError(Exception):
+ def __init__(self, status=None, message=None):
+ """Is initiated with a (notmuch.STATUS[,message=None])"""
+ super(NotmuchError, self).__init__(message, status)
+
+ def __str__(self):
+ if self.args[0] is not None: return self.args[0]
+ else: return STATUS.status2str(self.args[1])
+
diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py
new file mode 100644
index 0000000..613cc4a
--- /dev/null
+++ b/bindings/python/notmuch/message.py
@@ -0,0 +1,777 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch. If not, see <http://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+ Jesse Rosenthal <jrosenthal@jhu.edu>
+"""
+
+
+from ctypes import c_char_p, c_void_p, c_long, c_uint
+from datetime import date
+from notmuch.globals import nmlib, STATUS, NotmuchError, Enum
+from notmuch.tag import Tags
+import sys
+import email
+import types
+try:
+ import simplejson as json
+except ImportError:
+ import json
+#------------------------------------------------------------------------------
+class Messages(object):
+ """Represents a list of notmuch messages
+
+ This object provides an iterator over a list of notmuch messages
+ (Technically, it provides a wrapper for the underlying
+ *notmuch_messages_t* structure). Do note that the underlying
+ library only provides a one-time iterator (it cannot reset the
+ iterator to the start). Thus iterating over the function will
+ "exhaust" the list of messages, and a subsequent iteration attempt
+ will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
+ note, that any function that uses iteration will also
+ exhaust the messages. So both::
+
+ for msg in msgs: print msg
+
+ as well as::
+
+ number_of_msgs = len(msgs)
+
+ will "exhaust" the Messages. If you need to re-iterate over a list of
+ messages you will need to retrieve a new :class:`Messages` object.
+
+ Things are not as bad as it seems though, you can store and reuse
+ the single Message objects as often as you want as long as you
+ keep the parent Messages object around. (Recall that due to
+ hierarchical memory allocation, all derived Message objects will
+ be invalid when we delete the parent Messages() object, even if it
+ was already "exhausted".) So this works::
+
+ db = Database()
+ msgs = Query(db,'').search_messages() #get a Messages() object
+ msglist = []
+ for m in msgs:
+ msglist.append(m)
+
+ # msgs is "exhausted" now and even len(msgs) will raise an exception.
+ # However it will be kept around until all retrieved Message() objects are
+ # also deleted. If you did e.g. an explicit del(msgs) here, the
+ # following lines would fail.
+
+ # You can reiterate over *msglist* however as often as you want.
+ # It is simply a list with Message objects.
+
+ print (msglist[0].get_filename())
+ print (msglist[1].get_filename())
+ print (msglist[0].get_message_id())
+ """
+
+ #notmuch_tags_get
+ _get = nmlib.notmuch_messages_get
+ _get.restype = c_void_p
+
+ _collect_tags = nmlib.notmuch_messages_collect_tags
+ _collect_tags.restype = c_void_p
+
+ def __init__(self, msgs_p, parent=None):
+ """
+ :param msgs_p: A pointer to an underlying *notmuch_messages_t*
+ structure. These are not publically exposed, so a user
+ will almost never instantiate a :class:`Messages` object
+ herself. They are usually handed back as a result,
+ e.g. in :meth:`Query.search_messages`. *msgs_p* must be
+ valid, we will raise an :exc:`NotmuchError`
+ (STATUS.NULL_POINTER) if it is `None`.
+ :type msgs_p: :class:`ctypes.c_void_p`
+ :param parent: The parent object
+ (ie :class:`Query`) these tags are derived from. It saves
+ a reference to it, so we can automatically delete the db
+ object once all derived objects are dead.
+ :TODO: Make the iterator work more than once and cache the tags in
+ the Python object.(?)
+ """
+ if msgs_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+
+ self._msgs = msgs_p
+ #store parent, so we keep them alive as long as self is alive
+ self._parent = parent
+
+ def collect_tags(self):
+ """Return the unique :class:`Tags` in the contained messages
+
+ :returns: :class:`Tags`
+ :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
+
+ .. note:: :meth:`collect_tags` will iterate over the messages and
+ therefore will not allow further iterations.
+ """
+ if self._msgs is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ # collect all tags (returns NULL on error)
+ tags_p = Messages._collect_tags (self._msgs)
+ #reset _msgs as we iterated over it and can do so only once
+ self._msgs = None
+
+ if tags_p == None:
+ raise NotmuchError(STATUS.NULL_POINTER)
+ return Tags(tags_p, self)
+
+ def __iter__(self):
+ """ Make Messages an iterator """
+ return self
+
+ def next(self):
+ if self._msgs is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ if not nmlib.notmuch_messages_valid(self._msgs):
+ self._msgs = None
+ raise StopIteration
+
+ msg = Message(Messages._get (self._msgs), self)
+ nmlib.notmuch_messages_move_to_next(self._msgs)
+ return msg
+
+ def __len__(self):
+ """len(:class:`Messages`) returns the number of contained messages
+
+ .. note:: As this iterates over the messages, we will not be able to
+ iterate over them again! So this will fail::
+
+ #THIS FAILS
+ msgs = Database().create_query('').search_message()
+ if len(msgs) > 0: #this 'exhausts' msgs
+ # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
+ for msg in msgs: print msg
+
+ Most of the time, using the
+ :meth:`Query.count_messages` is therefore more
+ appropriate (and much faster). While not guaranteeing
+ that it will return the exact same number than len(),
+ in my tests it effectively always did so.
+ """
+ if self._msgs is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ i=0
+ while nmlib.notmuch_messages_valid(self._msgs):
+ nmlib.notmuch_messages_move_to_next(self._msgs)
+ i += 1
+ self._msgs = None
+ return i
+
+ def __del__(self):
+ """Close and free the notmuch Messages"""
+ if self._msgs is not None:
+ nmlib.notmuch_messages_destroy (self._msgs)
+
+ def print_messages(self, format, indent=0, entire_thread=False):
+ """Outputs messages as needed for 'notmuch show' to sys.stdout
+
+ :param format: A string of either 'text' or 'json'.
+ :param indent: A number indicating the reply depth of these messages.
+ :param entire_thread: A bool, indicating whether we want to output
+ whole threads or only the matching messages.
+ """
+ if format.lower() == "text":
+ set_start = ""
+ set_end = ""
+ set_sep = ""
+ elif format.lower() == "json":
+ set_start = "["
+ set_end = "]"
+ set_sep = ", "
+ else:
+ raise Exception
+
+ first_set = True
+
+ sys.stdout.write(set_start)
+
+ # iterate through all toplevel messages in this thread
+ for msg in self:
+ # if not msg:
+ # break
+ if not first_set:
+ sys.stdout.write(set_sep)
+ first_set = False
+
+ sys.stdout.write(set_start)
+ match = msg.is_match()
+ next_indent = indent
+
+ if (match or entire_thread):
+ if format.lower() == "text":
+ sys.stdout.write(msg.format_message_as_text(indent))
+ elif format.lower() == "json":
+ sys.stdout.write(msg.format_message_as_json(indent))
+ else:
+ raise NotmuchError
+ next_indent = indent + 1
+
+ # get replies and print them also out (if there are any)
+ replies = msg.get_replies()
+ if not replies is None:
+ sys.stdout.write(set_sep)
+ replies.print_messages(format, next_indent, entire_thread)
+
+ sys.stdout.write(set_end)
+ sys.stdout.write(set_end)
+
+#------------------------------------------------------------------------------
+class Message(object):
+ """Represents a single Email message
+
+ Technically, this wraps the underlying *notmuch_message_t* structure.
+ """
+
+ """notmuch_message_get_filename (notmuch_message_t *message)"""
+ _get_filename = nmlib.notmuch_message_get_filename
+ _get_filename.restype = c_char_p
+
+ """notmuch_message_get_flag"""
+ _get_flag = nmlib.notmuch_message_get_flag
+ _get_flag.restype = c_uint
+
+ """notmuch_message_get_message_id (notmuch_message_t *message)"""
+ _get_message_id = nmlib.notmuch_message_get_message_id
+ _get_message_id.restype = c_char_p
+
+ """notmuch_message_get_thread_id"""
+ _get_thread_id = nmlib.notmuch_message_get_thread_id
+ _get_thread_id.restype = c_char_p
+
+ """notmuch_message_get_replies"""
+ _get_replies = nmlib.notmuch_message_get_replies
+ _get_replies.restype = c_void_p
+
+ """notmuch_message_get_tags (notmuch_message_t *message)"""
+ _get_tags = nmlib.notmuch_message_get_tags
+ _get_tags.restype = c_void_p
+
+ _get_date = nmlib.notmuch_message_get_date
+ _get_date.restype = c_long
+
+ _get_header = nmlib.notmuch_message_get_header
+ _get_header.restype = c_char_p
+
+ #Constants: Flags that can be set/get with set_flag
+ FLAG = Enum(['MATCH'])
+
+ def __init__(self, msg_p, parent=None):
+ """
+ :param msg_p: A pointer to an internal notmuch_message_t
+ Structure. If it is `None`, we will raise an :exc:`NotmuchError`
+ STATUS.NULL_POINTER.
+ :param parent: A 'parent' object is passed which this message is
+ derived from. We save a reference to it, so we can
+ automatically delete the parent object once all derived
+ objects are dead.
+ """
+ if msg_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+ self._msg = msg_p
+ #keep reference to parent, so we keep it alive
+ self._parent = parent
+
+
+ def get_message_id(self):
+ """Returns the message ID
+
+ :returns: String with a message ID
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Message._get_message_id(self._msg)
+
+ def get_thread_id(self):
+ """Returns the thread ID
+
+ The returned string belongs to 'message' will only be valid for as
+ long as the message is valid.
+
+ This function will not return None since Notmuch ensures that every
+ message belongs to a single thread.
+
+ :returns: String with a thread ID
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ return Message._get_thread_id (self._msg);
+
+ def get_replies(self):
+ """Gets all direct replies to this message as :class:`Messages` iterator
+
+ .. note:: This call only makes sense if 'message' was
+ ultimately obtained from a :class:`Thread` object, (such as
+ by coming directly from the result of calling
+ :meth:`Thread.get_toplevel_messages` or by any number of
+ subsequent calls to :meth:`get_replies`). If this message was
+ obtained through some non-thread means, (such as by a call
+ to :meth:`Query.search_messages`), then this function will
+ return `None`.
+
+ :returns: :class:`Messages` or `None` if there are no replies to
+ this message.
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ msgs_p = Message._get_replies(self._msg);
+
+ if msgs_p is None:
+ return None
+
+ return Messages(msgs_p,self)
+
+ def get_date(self):
+ """Returns time_t of the message date
+
+ For the original textual representation of the Date header from the
+ message call notmuch_message_get_header() with a header value of
+ "date".
+
+ :returns: A time_t timestamp.
+ :rtype: c_unit64
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Message._get_date(self._msg)
+
+ def get_header(self, header):
+ """Returns a message header
+
+ This returns any message header that is stored in the notmuch database.
+ This is only a selected subset of headers, which is currently:
+
+ TODO: add stored headers
+
+ :param header: The name of the header to be retrieved.
+ It is not case-sensitive (TODO: confirm).
+ :type header: str
+ :returns: The header value as string
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ * STATUS.NULL_POINTER, if no header was found
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ #Returns NULL if any error occurs.
+ header = Message._get_header (self._msg, header)
+ if header == None:
+ raise NotmuchError(STATUS.NULL_POINTER)
+ return header
+
+ def get_filename(self):
+ """Returns the file path of the message file
+
+ :returns: Absolute file path & name of the message file
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Message._get_filename(self._msg)
+
+ def get_flag(self, flag):
+ """Checks whether a specific flag is set for this message
+
+ The method :meth:`Query.search_threads` sets
+ *Message.FLAG.MATCH* for those messages that match the
+ query. This method allows us to get the value of this flag.
+
+ :param flag: One of the :attr:`Message.FLAG` values (currently only
+ *Message.FLAG.MATCH*
+ :returns: An unsigned int (0/1), indicating whether the flag is set.
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Message._get_flag(self._msg, flag)
+
+ def set_flag(self, flag, value):
+ """Sets/Unsets a specific flag for this message
+
+ :param flag: One of the :attr:`Message.FLAG` values (currently only
+ *Message.FLAG.MATCH*
+ :param value: A bool indicating whether to set or unset the flag.
+
+ :returns: Nothing
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ nmlib.notmuch_message_set_flag(self._msg, flag, value)
+
+ def get_tags(self):
+ """Returns the message tags
+
+ :returns: A :class:`Tags` iterator.
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ * STATUS.NULL_POINTER, on error
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ tags_p = Message._get_tags(self._msg)
+ if tags_p == None:
+ raise NotmuchError(STATUS.NULL_POINTER)
+ return Tags(tags_p, self)
+
+ def add_tag(self, tag):
+ """Adds a tag to the given message
+
+ Adds a tag to the current message. The maximal tag length is defined in
+ the notmuch library and is currently 200 bytes.
+
+ :param tag: String with a 'tag' to be added.
+ :returns: STATUS.SUCCESS if the tag was successfully added.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.NULL_POINTER
+ The 'tag' argument is NULL
+ STATUS.TAG_TOO_LONG
+ The length of 'tag' is too long
+ (exceeds Message.NOTMUCH_TAG_MAX)
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so message cannot be
+ modified.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_add_tag (self._msg, tag)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+ def remove_tag(self, tag):
+ """Removes a tag from the given message
+
+ If the message has no such tag, this is a non-operation and
+ will report success anyway.
+
+ :param tag: String with a 'tag' to be removed.
+ :returns: STATUS.SUCCESS if the tag was successfully removed or if
+ the message had no such tag.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.NULL_POINTER
+ The 'tag' argument is NULL
+ STATUS.TAG_TOO_LONG
+ The length of 'tag' is too long
+ (exceeds NOTMUCH_TAG_MAX)
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so message cannot
+ be modified.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_remove_tag(self._msg, tag)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+ def remove_all_tags(self):
+ """Removes all tags from the given message.
+
+ See :meth:`freeze` for an example showing how to safely
+ replace tag values.
+
+ :returns: STATUS.SUCCESS if the tags were successfully removed.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so message cannot
+ be modified.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_remove_all_tags(self._msg)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+ def freeze(self):
+ """Freezes the current state of 'message' within the database
+
+ This means that changes to the message state, (via :meth:`add_tag`,
+ :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
+ committed to the database until the message is :meth:`thaw`ed.
+
+ Multiple calls to freeze/thaw are valid and these calls will
+ "stack". That is there must be as many calls to thaw as to freeze
+ before a message is actually thawed.
+
+ The ability to do freeze/thaw allows for safe transactions to
+ change tag values. For example, explicitly setting a message to
+ have a given set of tags might look like this::
+
+ msg.freeze()
+ msg.remove_all_tags()
+ for tag in new_tags:
+ msg.add_tag(tag)
+ msg.thaw()
+
+ With freeze/thaw used like this, the message in the database is
+ guaranteed to have either the full set of original tag values, or
+ the full set of new tag values, but nothing in between.
+
+ Imagine the example above without freeze/thaw and the operation
+ somehow getting interrupted. This could result in the message being
+ left with no tags if the interruption happened after
+ :meth:`remove_all_tags` but before :meth:`add_tag`.
+
+ :returns: STATUS.SUCCESS if the message was successfully frozen.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so message cannot
+ be modified.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_freeze(self._msg)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+ def thaw(self):
+ """Thaws the current 'message'
+
+ Thaw the current 'message', synchronizing any changes that may have
+ occurred while 'message' was frozen into the notmuch database.
+
+ See :meth:`freeze` for an example of how to use this
+ function to safely provide tag changes.
+
+ Multiple calls to freeze/thaw are valid and these calls with
+ "stack". That is there must be as many calls to thaw as to freeze
+ before a message is actually thawed.
+
+ :returns: STATUS.SUCCESS if the message was successfully frozen.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.UNBALANCED_FREEZE_THAW
+ An attempt was made to thaw an unfrozen message.
+ That is, there have been an unbalanced number of calls
+ to :meth:`freeze` and :meth:`thaw`.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_thaw(self._msg)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+
+ def is_match(self):
+ """(Not implemented)"""
+ return self.get_flag(Message.FLAG.MATCH)
+
+ def __str__(self):
+ """A message() is represented by a 1-line summary"""
+ msg = {}
+ msg['from'] = self.get_header('from')
+ msg['tags'] = str(self.get_tags())
+ msg['date'] = date.fromtimestamp(self.get_date())
+ replies = self.get_replies()
+ msg['replies'] = len(replies) if replies is not None else -1
+ return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg)
+
+
+ def get_message_parts(self):
+ """Output like notmuch show"""
+ fp = open(self.get_filename())
+ email_msg = email.message_from_file(fp)
+ fp.close()
+
+ out = []
+ for msg in email_msg.walk():
+ if not msg.is_multipart():
+ out.append(msg)
+ return out
+
+ def get_part(self, num):
+ """Returns the nth message body part"""
+ parts = self.get_message_parts()
+ if (num <= 0 or num > len(parts)):
+ return ""
+ else:
+ out_part = parts[(num - 1)]
+ return out_part.get_payload(decode=True)
+
+ def format_message_internal(self):
+ """Create an internal representation of the message parts,
+ which can easily be output to json, text, or another output
+ format. The argument match tells whether this matched a
+ query."""
+ output = {}
+ output["id"] = self.get_message_id()
+ output["match"] = self.is_match()
+ output["filename"] = self.get_filename()
+ output["tags"] = list(self.get_tags())
+
+ headers = {}
+ for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
+ headers[h] = self.get_header(h)
+ output["headers"] = headers
+
+ body = []
+ parts = self.get_message_parts()
+ for i in xrange(len(parts)):
+ msg = parts[i]
+ part_dict = {}
+ part_dict["id"] = i + 1
+ # We'll be using this is a lot, so let's just get it once.
+ cont_type = msg.get_content_type()
+ part_dict["content-type"] = cont_type
+ # NOTE:
+ # Now we emulate the current behaviour, where it ignores
+ # the html if there's a text representation.
+ #
+ # This is being worked on, but it will be easier to fix
+ # here in the future than to end up with another
+ # incompatible solution.
+ disposition = msg["Content-Disposition"]
+ if disposition and disposition.lower().startswith("attachment"):
+ part_dict["filename"] = msg.get_filename()
+ else:
+ if cont_type.lower() == "text/plain":
+ part_dict["content"] = msg.get_payload()
+ elif (cont_type.lower() == "text/html" and
+ i == 0):
+ part_dict["content"] = msg.get_payload()
+ body.append(part_dict)
+
+ output["body"] = body
+
+ return output
+
+ def format_message_as_json(self, indent=0):
+ """Outputs the message as json. This is essentially the same
+ as python's dict format, but we run it through, just so we
+ don't have to worry about the details."""
+ return json.dumps(self.format_message_internal())
+
+ def format_message_as_text(self, indent=0):
+ """Outputs it in the old-fashioned notmuch text form. Will be
+ easy to change to a new format when the format changes."""
+
+ format = self.format_message_internal()
+ output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
+ % (format['id'], indent, format['match'], format['filename'])
+ output += "\n\fheader{"
+
+ #Todo: this date is supposed to be prettified, as in the index.
+ output += "\n%s (%s) (" % (format["headers"]["From"],
+ format["headers"]["Date"])
+ output += ", ".join(format["tags"])
+ output += ")"
+
+ output += "\nSubject: %s" % format["headers"]["Subject"]
+ output += "\nFrom: %s" % format["headers"]["From"]
+ output += "\nTo: %s" % format["headers"]["To"]
+ if format["headers"]["Cc"]:
+ output += "\nCc: %s" % format["headers"]["Cc"]
+ if format["headers"]["Bcc"]:
+ output += "\nBcc: %s" % format["headers"]["Bcc"]
+ output += "\nDate: %s" % format["headers"]["Date"]
+ output += "\n\fheader}"
+
+ output += "\n\fbody{"
+
+ parts = format["body"]
+ parts.sort(key=lambda(p): p["id"])
+ for p in parts:
+ if not p.has_key("filename"):
+ output += "\n\fpart{ "
+ output += "ID: %d, Content-type: %s\n" % (p["id"],
+ p["content-type"])
+ if p.has_key("content"):
+ output += "\n%s\n" % p["content"]
+ else:
+ output += "Non-text part: %s\n" % p["content-type"]
+ output += "\n\fpart}"
+ else:
+ output += "\n\fattachment{ "
+ output += "ID: %d, Content-type:%s\n" % (p["id"],
+ p["content-type"])
+ output += "Attachment: %s\n" % p["filename"]
+ output += "\n\fattachment}\n"
+
+ output += "\n\fbody}\n"
+ output += "\n\fmessage}"
+
+ return output
+
+ def __del__(self):
+ """Close and free the notmuch Message"""
+ if self._msg is not None:
+ nmlib.notmuch_message_destroy (self._msg)
diff --git a/bindings/python/notmuch/tag.py b/bindings/python/notmuch/tag.py
new file mode 100644
index 0000000..cf1152a
--- /dev/null
+++ b/bindings/python/notmuch/tag.py
@@ -0,0 +1,126 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch. If not, see <http://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+"""
+from ctypes import c_char_p
+from notmuch.globals import nmlib, STATUS, NotmuchError
+
+#------------------------------------------------------------------------------
+class Tags(object):
+ """Represents a list of notmuch tags
+
+ This object provides an iterator over a list of notmuch tags. Do
+ note that the underlying library only provides a one-time iterator
+ (it cannot reset the iterator to the start). Thus iterating over
+ the function will "exhaust" the list of tags, and a subsequent
+ iteration attempt will raise a :exc:`NotmuchError`
+ STATUS.NOT_INITIALIZED. Also note, that any function that uses
+ iteration (nearly all) will also exhaust the tags. So both::
+
+ for tag in tags: print tag
+
+ as well as::
+
+ number_of_tags = len(tags)
+
+ and even a simple::
+
+ #str() iterates over all tags to construct a space separated list
+ print(str(tags))
+
+ will "exhaust" the Tags. If you need to re-iterate over a list of
+ tags you will need to retrieve a new :class:`Tags` object.
+ """
+
+ #notmuch_tags_get
+ _get = nmlib.notmuch_tags_get
+ _get.restype = c_char_p
+
+ def __init__(self, tags_p, parent=None):
+ """
+ :param tags_p: A pointer to an underlying *notmuch_tags_t*
+ structure. These are not publically exposed, so a user
+ will almost never instantiate a :class:`Tags` object
+ herself. They are usually handed back as a result,
+ e.g. in :meth:`Database.get_all_tags`. *tags_p* must be
+ valid, we will raise an :exc:`NotmuchError`
+ (STATUS.NULL_POINTER) if it is `None`.
+ :type tags_p: :class:`ctypes.c_void_p`
+ :param parent: The parent object (ie :class:`Database` or
+ :class:`Message` these tags are derived from, and saves a
+ reference to it, so we can automatically delete the db object
+ once all derived objects are dead.
+ :TODO: Make the iterator optionally work more than once by
+ cache the tags in the Python object(?)
+ """
+ if tags_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+
+ self._tags = tags_p
+ #save reference to parent object so we keep it alive
+ self._parent = parent
+
+ def __iter__(self):
+ """ Make Tags an iterator """
+ return self
+
+ def next(self):
+ if self._tags is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ if not nmlib.notmuch_tags_valid(self._tags):
+ self._tags = None
+ raise StopIteration
+
+ tag = Tags._get (self._tags)
+ nmlib.notmuch_tags_move_to_next(self._tags)
+ return tag
+
+ def __len__(self):
+ """len(:class:`Tags`) returns the number of contained tags
+
+ .. note:: As this iterates over the tags, we will not be able
+ to iterate over them again (as in retrieve them)! If
+ the tags have been exhausted already, this will raise a
+ :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
+ subsequent attempts.
+ """
+ if self._tags is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ i=0
+ while nmlib.notmuch_tags_valid(self._msgs):
+ nmlib.notmuch_tags_move_to_next(self._msgs)
+ i += 1
+ self._tags = None
+ return i
+
+ def __str__(self):
+ """The str() representation of Tags() is a space separated list of tags
+
+ .. note:: As this iterates over the tags, we will not be able
+ to iterate over them again (as in retrieve them)! If
+ the tags have been exhausted already, this will raise a
+ :exc:`NotmuchError` STATUS.NOT_INITIALIZED on
+ subsequent attempts.
+ """
+ return " ".join(self)
+
+ def __del__(self):
+ """Close and free the notmuch tags"""
+ if self._tags is not None:
+ nmlib.notmuch_tags_destroy (self._tags)
diff --git a/bindings/python/notmuch/thread.py b/bindings/python/notmuch/thread.py
new file mode 100644
index 0000000..eebd6cb
--- /dev/null
+++ b/bindings/python/notmuch/thread.py
@@ -0,0 +1,370 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch. If not, see <http://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
+"""
+
+from ctypes import c_char_p, c_void_p, c_long
+from notmuch.globals import nmlib, STATUS, NotmuchError
+from notmuch.message import Messages
+from notmuch.tag import Tags
+from datetime import date
+
+#------------------------------------------------------------------------------
+class Threads(object):
+ """Represents a list of notmuch threads
+
+ This object provides an iterator over a list of notmuch threads
+ (Technically, it provides a wrapper for the underlying
+ *notmuch_threads_t* structure). Do note that the underlying
+ library only provides a one-time iterator (it cannot reset the
+ iterator to the start). Thus iterating over the function will
+ "exhaust" the list of threads, and a subsequent iteration attempt
+ will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
+ note, that any function that uses iteration will also
+ exhaust the messages. So both::
+
+ for thread in threads: print thread
+
+ as well as::
+
+ number_of_msgs = len(threads)
+
+ will "exhaust" the threads. If you need to re-iterate over a list of
+ messages you will need to retrieve a new :class:`Threads` object.
+
+ Things are not as bad as it seems though, you can store and reuse
+ the single Thread objects as often as you want as long as you
+ keep the parent Threads object around. (Recall that due to
+ hierarchical memory allocation, all derived Threads objects will
+ be invalid when we delete the parent Threads() object, even if it
+ was already "exhausted".) So this works::
+
+ db = Database()
+ threads = Query(db,'').search_threads() #get a Threads() object
+ threadlist = []
+ for thread in threads:
+ threadlist.append(thread)
+
+ # threads is "exhausted" now and even len(threads) will raise an
+ # exception.
+ # However it will be kept around until all retrieved Thread() objects are
+ # also deleted. If you did e.g. an explicit del(threads) here, the
+ # following lines would fail.
+
+ # You can reiterate over *threadlist* however as often as you want.
+ # It is simply a list with Thread objects.
+
+ print (threadlist[0].get_thread_id())
+ print (threadlist[1].get_thread_id())
+ print (threadlist[0].get_total_messages())
+ """
+
+ #notmuch_threads_get
+ _get = nmlib.notmuch_threads_get
+ _get.restype = c_void_p
+
+ def __init__(self, threads_p, parent=None):
+ """
+ :param threads_p: A pointer to an underlying *notmuch_threads_t*
+ structure. These are not publically exposed, so a user
+ will almost never instantiate a :class:`Threads` object
+ herself. They are usually handed back as a result,
+ e.g. in :meth:`Query.search_threads`. *threads_p* must be
+ valid, we will raise an :exc:`NotmuchError`
+ (STATUS.NULL_POINTER) if it is `None`.
+ :type threads_p: :class:`ctypes.c_void_p`
+ :param parent: The parent object
+ (ie :class:`Query`) these tags are derived from. It saves
+ a reference to it, so we can automatically delete the db
+ object once all derived objects are dead.
+ :TODO: Make the iterator work more than once and cache the tags in
+ the Python object.(?)
+ """
+ if threads_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+
+ self._threads = threads_p
+ #store parent, so we keep them alive as long as self is alive
+ self._parent = parent
+
+ def __iter__(self):
+ """ Make Threads an iterator """
+ return self
+
+ def next(self):
+ if self._threads is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ if not nmlib.notmuch_threads_valid(self._threads):
+ self._threads = None
+ raise StopIteration
+
+ thread = Thread(Threads._get (self._threads), self)
+ nmlib.notmuch_threads_move_to_next(self._threads)
+ return thread
+
+ def __len__(self):
+ """len(:class:`Threads`) returns the number of contained Threads
+
+ .. note:: As this iterates over the threads, we will not be able to
+ iterate over them again! So this will fail::
+
+ #THIS FAILS
+ threads = Database().create_query('').search_threads()
+ if len(threads) > 0: #this 'exhausts' threads
+ # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
+ for thread in threads: print thread
+ """
+ if self._threads is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ i=0
+ # returns 'bool'. On out-of-memory it returns None
+ while nmlib.notmuch_threads_valid(self._threads):
+ nmlib.notmuch_threads_move_to_next(self._threads)
+ i += 1
+ # reset self._threads to mark as "exhausted"
+ self._threads = None
+ return i
+
+
+
+ def __del__(self):
+ """Close and free the notmuch Threads"""
+ if self._threads is not None:
+ nmlib.notmuch_messages_destroy (self._threads)
+
+#------------------------------------------------------------------------------
+class Thread(object):
+ """Represents a single message thread."""
+
+ """notmuch_thread_get_thread_id"""
+ _get_thread_id = nmlib.notmuch_thread_get_thread_id
+ _get_thread_id.restype = c_char_p
+
+ """notmuch_thread_get_authors"""
+ _get_authors = nmlib.notmuch_thread_get_authors
+ _get_authors.restype = c_char_p
+
+ """notmuch_thread_get_subject"""
+ _get_subject = nmlib.notmuch_thread_get_subject
+ _get_subject.restype = c_char_p
+
+ """notmuch_thread_get_toplevel_messages"""
+ _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
+ _get_toplevel_messages.restype = c_void_p
+
+ _get_newest_date = nmlib.notmuch_thread_get_newest_date
+ _get_newest_date.restype = c_long
+
+ _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
+ _get_oldest_date.restype = c_long
+
+ """notmuch_thread_get_tags"""
+ _get_tags = nmlib.notmuch_thread_get_tags
+ _get_tags.restype = c_void_p
+
+ def __init__(self, thread_p, parent=None):
+ """
+ :param thread_p: A pointer to an internal notmuch_thread_t
+ Structure. These are not publically exposed, so a user
+ will almost never instantiate a :class:`Thread` object
+ herself. They are usually handed back as a result,
+ e.g. when iterating through :class:`Threads`. *thread_p*
+ must be valid, we will raise an :exc:`NotmuchError`
+ (STATUS.NULL_POINTER) if it is `None`.
+
+ :param parent: A 'parent' object is passed which this message is
+ derived from. We save a reference to it, so we can
+ automatically delete the parent object once all derived
+ objects are dead.
+ """
+ if thread_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+ self._thread = thread_p
+ #keep reference to parent, so we keep it alive
+ self._parent = parent
+
+ def get_thread_id(self):
+ """Get the thread ID of 'thread'
+
+ The returned string belongs to 'thread' and will only be valid
+ for as long as the thread is valid.
+
+ :returns: String with a message ID
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
+ is not initialized.
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Thread._get_thread_id(self._thread)
+
+ def get_total_messages(self):
+ """Get the total number of messages in 'thread'
+
+ :returns: The number of all messages in the database
+ belonging to this thread. Contrast with
+ :meth:`get_matched_messages`.
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
+ is not initialized.
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return nmlib.notmuch_thread_get_total_messages(self._thread)
+
+
+ def get_toplevel_messages(self):
+ """Returns a :class:`Messages` iterator for the top-level messages in
+ 'thread'
+
+ This iterator will not necessarily iterate over all of the messages
+ in the thread. It will only iterate over the messages in the thread
+ which are not replies to other messages in the thread.
+
+ To iterate over all messages in the thread, the caller will need to
+ iterate over the result of :meth:`Message.get_replies` for each
+ top-level message (and do that recursively for the resulting
+ messages, etc.).
+
+ :returns: :class:`Messages`
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if query is not inited
+ * STATUS.NULL_POINTER if search_messages failed
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ msgs_p = Thread._get_toplevel_messages(self._thread)
+
+ if msgs_p is None:
+ NotmuchError(STATUS.NULL_POINTER)
+
+ return Messages(msgs_p,self)
+
+ def get_matched_messages(self):
+ """Returns the number of messages in 'thread' that matched the query
+
+ :returns: The number of all messages belonging to this thread that
+ matched the :class:`Query`from which this thread was created.
+ Contrast with :meth:`get_total_messages`.
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
+ is not initialized.
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return nmlib.notmuch_thread_get_matched_messages(self._thread)
+
+ def get_authors(self):
+ """Returns the authors of 'thread'
+
+ The returned string is a comma-separated list of the names of the
+ authors of mail messages in the query results that belong to this
+ thread.
+
+ The returned string belongs to 'thread' and will only be valid for
+ as long as this Thread() is not deleted.
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Thread._get_authors(self._thread)
+
+ def get_subject(self):
+ """Returns the Subject of 'thread'
+
+ The returned string belongs to 'thread' and will only be valid for
+ as long as this Thread() is not deleted.
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Thread._get_subject(self._thread)
+
+ def get_newest_date(self):
+ """Returns time_t of the newest message date
+
+ :returns: A time_t timestamp.
+ :rtype: c_unit64
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Thread._get_newest_date(self._thread)
+
+ def get_oldest_date(self):
+ """Returns time_t of the oldest message date
+
+ :returns: A time_t timestamp.
+ :rtype: c_unit64
+ :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
+ is not initialized.
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+ return Thread._get_oldest_date(self._thread)
+
+ def get_tags(self):
+ """ Returns the message tags
+
+ In the Notmuch database, tags are stored on individual
+ messages, not on threads. So the tags returned here will be all
+ tags of the messages which matched the search and which belong to
+ this thread.
+
+ The :class:`Tags` object is owned by the thread and as such, will only
+ be valid for as long as this :class:`Thread` is valid (e.g. until the
+ query from which it derived is explicitely deleted).
+
+ :returns: A :class:`Tags` iterator.
+ :exception: :exc:`NotmuchError`
+
+ * STATUS.NOT_INITIALIZED if the thread
+ is not initialized.
+ * STATUS.NULL_POINTER, on error
+ """
+ if self._thread is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ tags_p = Thread._get_tags(self._thread)
+ if tags_p == None:
+ raise NotmuchError(STATUS.NULL_POINTER)
+ return Tags(tags_p, self)
+
+ def __str__(self):
+ """A str(Thread()) is represented by a 1-line summary"""
+ thread = {}
+ thread['id'] = self.get_thread_id()
+
+ ###TODO: How do we find out the current sort order of Threads?
+ ###Add a "sort" attribute to the Threads() object?
+ #if (sort == NOTMUCH_SORT_OLDEST_FIRST)
+ # date = notmuch_thread_get_oldest_date (thread);
+ #else
+ # date = notmuch_thread_get_newest_date (thread);
+ thread['date'] = date.fromtimestamp(self.get_newest_date())
+ thread['matched'] = self.get_matched_messages()
+ thread['total'] = self.get_total_messages()
+ thread['authors'] = self.get_authors()
+ thread['subject'] = self.get_subject()
+ thread['tags'] = self.get_tags()
+
+ return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread)
+
+ def __del__(self):
+ """Close and free the notmuch Thread"""
+ if self._thread is not None:
+ nmlib.notmuch_thread_destroy (self._thread)