]> git.notmuchmail.org Git - notmuch/commitdiff
cnotmuch -> notmuch
authorSebastian Spaeth <sebastian@sspaeth.de>
Mon, 19 Apr 2010 19:14:47 +0000 (21:14 +0200)
committerSebastian Spaeth <sebastian@sspaeth.de>
Mon, 19 Apr 2010 19:14:47 +0000 (21:14 +0200)
and GPL v3+ boilerplate code to each source file.

--HG--
rename : cnotmuch/__init__.py => notmuch/__init__.py
rename : cnotmuch/database.py => notmuch/database.py
rename : cnotmuch/globals.py => notmuch/globals.py
rename : cnotmuch/message.py => notmuch/message.py
rename : cnotmuch/notmuch.py => notmuch/notmuch.py
rename : cnotmuch/tag.py => notmuch/tag.py
rename : cnotmuch/thread.py => notmuch/thread.py

16 files changed:
cnotmuch/__init__.py [deleted file]
cnotmuch/database.py [deleted file]
cnotmuch/globals.py [deleted file]
cnotmuch/message.py [deleted file]
cnotmuch/notmuch.py [deleted file]
cnotmuch/tag.py [deleted file]
cnotmuch/thread.py [deleted file]
notmuch [deleted file]
notmuch/__init__.py [new file with mode: 0644]
notmuch/database.py [new file with mode: 0644]
notmuch/globals.py [new file with mode: 0644]
notmuch/message.py [new file with mode: 0644]
notmuch/notmuch.py [new file with mode: 0644]
notmuch/tag.py [new file with mode: 0644]
notmuch/thread.py [new file with mode: 0644]
setup.py

diff --git a/cnotmuch/__init__.py b/cnotmuch/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/cnotmuch/database.py b/cnotmuch/database.py
deleted file mode 100644 (file)
index fd2d9a9..0000000
+++ /dev/null
@@ -1,822 +0,0 @@
-import os
-from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref
-from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
-from cnotmuch.thread import Threads
-from cnotmuch.message import Messages, Message
-from cnotmuch.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 "<cnotmuch 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/cnotmuch/globals.py b/cnotmuch/globals.py
deleted file mode 100644 (file)
index fa20ae8..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-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/cnotmuch/message.py b/cnotmuch/message.py
deleted file mode 100644 (file)
index 0e5057f..0000000
+++ /dev/null
@@ -1,774 +0,0 @@
-#    This file is part of cnotmuch.
-#
-#    cnotmuch 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.
-#
-#    cnotmuch 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 cnotmuch.  If not, see <http://www.gnu.org/licenses/>.
-#
-#    (C) 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 cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
-from cnotmuch.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/cnotmuch/notmuch.py b/cnotmuch/notmuch.py
deleted file mode 100644 (file)
index f6a9a23..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-"""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`.
-
-"""
-from database import Database, Query
-from message import Messages, Message
-from thread import Threads, Thread
-from tag import Tags
-from cnotmuch.globals import nmlib, STATUS, NotmuchError
-__LICENSE__="GPL v3+"
-__VERSION__='0.2.2'
-__AUTHOR__ ='Sebastian Spaeth <Sebastian@SSpaeth.de>'
diff --git a/cnotmuch/tag.py b/cnotmuch/tag.py
deleted file mode 100644 (file)
index 00898ef..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-from ctypes import c_char_p
-from cnotmuch.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/cnotmuch/thread.py b/cnotmuch/thread.py
deleted file mode 100644 (file)
index 5a2505c..0000000
+++ /dev/null
@@ -1,351 +0,0 @@
-from ctypes import c_char_p, c_void_p, c_long
-from cnotmuch.globals import nmlib, STATUS, NotmuchError
-from cnotmuch.message import Messages
-from cnotmuch.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)
diff --git a/notmuch b/notmuch
deleted file mode 100755 (executable)
index 73ee33b..0000000
--- a/notmuch
+++ /dev/null
@@ -1,653 +0,0 @@
-#!/usr/bin/env python
-"""This is a notmuch implementation in python.
-It's goal is to allow running the test suite on the cnotmuch python bindings.
-
-This "binary" honors the NOTMUCH_CONFIG environmen variable for reading a user's
-notmuch configuration (e.g. the database path).
-
-   (c) 2010 by Sebastian Spaeth <Sebastian@SSpaeth.de>
-               Jesse Rosenthal <jrosenthal@jhu.edu>
-   This code is licensed under the GNU GPL v3+.
-"""
-import sys
-import os
-
-import re
-import stat
-import email
-
-from cnotmuch.notmuch import Database, Query, NotmuchError, STATUS
-from ConfigParser import SafeConfigParser
-from cStringIO import StringIO
-
-PREFIX = re.compile('(\w+):(.*$)')
-
-HELPTEXT = """The notmuch mail system.
-Usage: notmuch <command> [args...]
-
-Where <command> and [args...] are as follows:
-       setup   Interactively setup notmuch for first use.
-       new     [--verbose]
-               Find and import new messages to the notmuch database.
-       search  [options...] <search-terms> [...]
-               Search for messages matching the given search terms.
-       show    <search-terms> [...]
-               Show all messages matching the search terms.
-       count   <search-terms> [...]
-               Count messages matching the search terms.
-       reply   [options...] <search-terms> [...]
-               Construct a reply template for a set of messages.
-       tag     +<tag>|-<tag> [...] [--] <search-terms> [...]
-               Add/remove tags for all messages matching the search terms.
-       dump    [<filename>]
-               Create a plain-text dump of the tags for each message.
-       restore <filename>
-               Restore the tags from the given dump file (see 'dump').
-       search-tags     [<search-terms> [...] ]
-               List all tags found in the database or matching messages.
-       help    [<command>]
-               This message, or more detailed help for the named command.
-
-Use "notmuch help <command>" for more details on each command.
-And "notmuch help search-terms" for the common search-terms syntax.
-"""
-
-USAGE = """Notmuch is configured and appears to have a database. Excellent!
-
-At this point you can start exploring the functionality of notmuch by
-using commands such as:
-       notmuch search tag:inbox
-       notmuch search to:"%(fullname)s"
-       notmuch search from:"%(mailaddress)s"
-       notmuch search subject:"my favorite things"
-
-See "notmuch help search" for more details.
-
-You can also use "notmuch show" with any of the thread IDs resulting
-from a search. Finally, you may want to explore using a more sophisticated
-interface to notmuch such as the emacs interface implemented in notmuch.el
-or any other interface described at http://notmuchmail.org
-
-And don't forget to run "notmuch new" whenever new mail arrives.
-
-Have fun, and may your inbox never have much mail.
-"""
-
-#-------------------------------------------------------------------------
-def quote_query_line(argv):
-    # mangle arguments wrapping terms with spaces in quotes
-    for (num, item) in enumerate(argv):
-        if item.find(' ') >= 0:
-               # if we use prefix:termWithSpaces, put quotes around term
-               match = PREFIX.match(item)
-                if match:
-                       argv[num] = '%s:"%s"' %(match.group(1), match.group(2))
-               else:
-                       argv[num] = '"%s"' % item
-    return ' '.join(argv)
-
-#-------------------------------------------------------------------------
-
-
-class Notmuch(object):
-
-    def __init__(self, configpath="~/.notmuch-config)"):
-        self._config = None
-        self._configpath = os.getenv('NOTMUCH_CONFIG',
-            os.path.expanduser(configpath))
-
-    def cmd_usage(self):
-        """Print the usage text and exits"""
-        data={}
-        names = self.get_user_email_addresses()
-        data['fullname'] = names[0] if names[0] else 'My Name'
-        data['mailaddress'] = names[1] if names[1] else 'My@email.address'
-        print USAGE % data
-
-    def cmd_new(self):
-        """Run 'notmuch new'"""
-        #get the database directory
-        db = Database(mode=Database.MODE.READ_WRITE)
-        path = db.get_path()
-        print self._add_new_files_recursively(path, db)
-
-    def cmd_help(self, subcmd=None):
-        """Print help text for 'notmuch help'"""
-        if len(subcmd) > 1:
-            print "Help for specific commands not implemented"
-            return
-        print HELPTEXT
-
-    def _get_user_notmuch_config(self):
-        """Returns the ConfigParser of the user's notmuch-config"""
-       # return the cached config parser if we read it already
-       if self._config:
-            return self._config
-
-       config = SafeConfigParser()
-       config.read(self._configpath)
-       self._config = config
-       return config
-
-    def _add_new_files_recursively(self, path, db):
-        """:returns: (added, moved, removed)"""
-        print "Enter add new files with path %s" % path
-
-        try:
-            #get the Directory() object for this path
-            db_dir = db.get_directory(path)
-            added = moved = removed = 0
-        except NotmuchError:
-            # Occurs if we have wrong absolute paths in the db, for example
-            return (0,0,0)
-
-
-        # for folder in subdirs:
-
-        # TODO, retrieve dir mtime here and store it later
-        # as long as Filenames() does not allow multiple iteration, we need to
-        # use this kludgy way to get a sorted list of filenames
-        # db_files is a list of subdirectories and filenames in this folder
-        db_files = set()
-        db_folders = set()
-        for subdir in db_dir.get_child_directories():
-            db_folders.add(subdir)
-# file is a keyword (remove this ;))
-        for mail in db_dir.get_child_files():
-            db_files.add(mail)
-
-        fs_files = set(os.listdir(db_dir.path))
-
-        # list of files (and folders) on the fs, but not the db
-        for fs_file in ((fs_files - db_files) - db_folders):
-            absfile = os.path.normpath(os.path.join(db_dir.path, fs_file))
-            statinfo = os.stat(absfile)
-
-            if stat.S_ISDIR(statinfo.st_mode):
-                # This is a directory
-                if fs_file in ['.notmuch','tmp','.']:
-                    continue
-               print "%s %s" % (fs_file, db_folders)
-                print "Directory not in db yet. Descending into %s" % absfile
-                new = self._add_new_files_recursively(absfile, db)
-                added += new[0]
-                moved += new[1]
-                removed += new[2]
-
-            elif stat.S_ISLNK(statinfo.st_mode):
-                print ("%s is a symbolic link (%d). FIXME!!!" %
-                       (absfile, statinfo.st_mode))
-                exit(1)
-
-            else:
-                # This is a regular file, not in the db yet. Add it.
-                print "This file needs to be added %s" % (absfile)
-                (msg, status) = db.add_message(absfile)
-                # We increases 'added', even on dupe messages. If it is a moved
-                # message, we will deduct it later and increase 'moved' instead
-                added += 1
-
-                if status == STATUS.DUPLICATE_MESSAGE_ID:
-                    print "Added msg was in the db"
-                else:
-                    print "New message."
-
-        # Finally a list of files (not folders) in the database,
-        # but not the filesystem
-        for db_file in (db_files - fs_files):
-            absfile = os.path.normpath(os.path.join(db_dir.path, db_file))
-
-            # remove a mail message from the db
-            print ("%s is not on the fs anymore. Delete" % absfile)
-            status = db.remove_message(absfile)
-
-            if status == STATUS.SUCCESS:
-                # we just deleted the last reference, so this was a remove
-                removed += 1
-                sys.stderr.write("SUCCESS %d %s %s.\n" %
-                        (status, STATUS.status2str(status), absfile))
-            elif status == STATUS.DUPLICATE_MESSAGE_ID:
-                # The filename exists already somewhere else, so this is a move
-                moved += 1
-                added -= 1
-                sys.stderr.write("DUPE %d %s %s.\n" %
-                        (status, STATUS.status2str(status), absfile))
-            else:
-                # This should not occur
-                sys.stderr.write("This should not occur %d %s %s.\n" %
-                        (status, STATUS.status2str(status), absfile))
-
-        # list of folders in the filesystem. Just descend into dirs
-        for fs_file in fs_files:
-            absfile = os.path.normpath(os.path.join(db_dir.path, fs_file))
-            if os.path.isdir(absfile):
-                # This is a directory. Remove it from the db_folder list. 
-                # All remaining db_folders at the end will be not present
-                # on the file system.
-                db_folders.remove(fs_file)
-                if fs_file in ['.notmuch','tmp','.']:
-                    continue
-                new = self._add_new_files_recursively(absfile, db)
-                added += new[0]
-                moved += new[0]
-                removed += new[0]
-
-        # we are not interested in anything but directories here
-        #TODO: All remaining elements of db_folders are not in the filesystem
-        #delete those
-
-        return added, moved, removed
-        #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.
-
-    def get_user_email_addresses(self):
-        """ Reads a user's notmuch config and returns his email addresses as
-       list (name, primary_address, other_address1,...)"""
-
-       #read the config file
-       config = self._get_user_notmuch_config()
-
-        conf = {'name': '', 'primary_email': ''}
-        for entry in conf:
-            if config.has_option('user', entry):
-                conf[entry] = config.get('user', entry)
-
-       if config.has_option('user','other_email'):
-            other = config.get('user','other_email')
-            other = [mail.strip() for mail in other.split(';') if mail]
-        else:
-            other = []
-        # for being compatible. It would be nicer to return a dict.
-       return conf.keys() + other
-
-    def quote_msg_body(self, oldbody ,date, from_address):
-        """Transform a mail body into a quoted text,
-        starting with On foo, bar wrote:
-
-        :param body: a str with a mail body
-        :returns: The new payload of the email.message()
-        """
-
-        # we get handed a string, wrap it in a file-like object
-        oldbody = StringIO(oldbody)
-        newbody = StringIO()
-
-        newbody.write("On %s, %s wrote:\n" % (date, from_address))
-
-        for line in oldbody:
-            newbody.write("> " + line)
-
-        return newbody.getvalue()
-
-    def format_reply(self, msgs):
-        """Gets handed Messages() and displays the reply to them
-
-        This is pretty ugly and hacky. It tries to mimic the "real"
-        notmuch output as much as it can to pass the test suite. It
-        could deserve a healthy bit of love.  It is also buggy because
-        it returns after the first message it has handled."""
-
-        for msg in msgs:
-            f = open(msg.get_filename(), "r")
-            reply = email.message_from_file(f)
-
-            # handle the easy non-multipart case:
-            if not reply.is_multipart():
-                reply.set_payload(self.quote_msg_body(reply.get_payload(),
-                    reply['date'], reply['from']))
-            else:
-                # handle the tricky multipart case
-                deleted = ""
-                """A string describing which nontext attachements
-                that have been deleted"""
-                delpayloads = []
-                """A list of payload indices to be deleted"""
-                payloads = reply.get_payload()
-
-                for (num, part) in enumerate(payloads):
-                    mime_main = part.get_content_maintype()
-                    if mime_main not in ['multipart', 'message', 'text']:
-                        deleted += "Non-text part: %s\n" % (part.get_content_type())
-                        payloads[num].set_payload("Non-text part: %s" %
-                                (part.get_content_type()))
-                        payloads[num].set_type('text/plain')
-                        delpayloads.append(num)
-                    elif mime_main == 'text':
-                        payloads[num].set_payload(self.quote_msg_body(
-                            payloads[num].get_payload(),
-                            reply['date'], reply['from']))
-                    else:
-                        # TODO handle deeply nested multipart messages
-                        sys.stderr.write ("FIXME: Ignoring multipart part. Handle me\n")
-                # Delete those payloads that we don't need anymore
-                for item in reversed(sorted(delpayloads)):
-                    del payloads[item]
-
-        # Back to single- and multipart handling
-        my_addresses = self.get_user_email_addresses()
-        used_address = None
-        # filter our email addresses from all to: cc: and bcc: fields
-        # if we find one of "my" addresses being used, 
-        # it is stored in used_address
-        for header in ['To', 'CC', 'Bcc']:
-            if not header in reply:
-                #only handle fields that exist
-                continue
-            addresses = email.utils.getaddresses(reply.get_all(header, []))
-            purged_addr = []
-            for (name, mail) in addresses:
-                if mail in my_addresses[1:]:
-                    used_address = email.utils.formataddr(
-                            (my_addresses[0], mail))
-                else:
-                    purged_addr.append(email.utils.formataddr((name, mail)))
-
-            if purged_addr:
-                reply.replace_header(header, ", ".join(purged_addr))
-            else:
-                # we deleted all addresses, delete the header
-                del reply[header]
-
-        # Use our primary email address to the From
-        # (save original from line, we still need it)
-        new_to = reply['From']
-        if used_address:
-            reply['From'] = used_address
-        else:
-            email.utils.formataddr((my_addresses[0], my_addresses[1]))
-
-        reply['Subject'] = 'Re: ' + reply['Subject']
-
-        # Calculate our new To: field
-        # add all remaining original 'To' addresses
-        if 'To' in reply:
-            new_to += ", " + reply['To']
-        reply.add_header('To', new_to)
-
-        # Add our primary email address to the BCC
-        new_bcc = my_addresses[1]
-        if 'Bcc' in reply:
-            new_bcc += ', '  + reply['Bcc']
-        reply['Bcc'] = new_bcc
-
-        # Set replies 'In-Reply-To' header to original's Message-ID
-        if 'Message-ID' in reply:
-            reply['In-Reply-To'] = reply['Message-ID']
-
-        #Add original's Message-ID to replies 'References' header.
-        if 'References' in reply:
-            reply['References'] =  ' '.join([reply['References'], reply['Message-ID']])
-        else:
-            reply['References'] = reply['Message-ID']
-
-        # Delete the original Message-ID.
-        del(reply['Message-ID'])
-
-        # filter all existing headers but a few and delete them from 'reply'
-        delheaders = filter(lambda x: x not in ['From', 'To', 'Subject', 'CC',
-                                                'Bcc', 'In-Reply-To',
-                                                'References', 'Content-Type'],
-                            reply.keys())
-        map(reply.__delitem__, delheaders)
-
-        # TODO: OUCH, we return after the first msg we have handled rather than
-        # handle all of them
-        # return resulting message without Unixfrom
-        return reply.as_string(False)
-
-
-def main():
-    # Handle command line options
-    #------------------------------------
-    # No option given, print USAGE and exit
-    if len(sys.argv) == 1:
-        Notmuch().cmd_usage()
-    #------------------------------------
-    elif sys.argv[1] == 'setup':
-       """Interactively setup notmuch for first use."""
-       exit("Not implemented.")
-    #-------------------------------------
-    elif sys.argv[1] == 'new':
-        """Check for new and removed messages."""
-        Notmuch().cmd_new()
-    #-------------------------------------
-    elif sys.argv[1] == 'help':
-        """Print the help text"""
-        Notmuch().cmd_help(sys.argv[1:])
-    #-------------------------------------
-    elif sys.argv[1] == 'part':
-        part()
-    #-------------------------------------
-    elif sys.argv[1] == 'search':
-        search()
-    #-------------------------------------
-    elif sys.argv[1] == 'show':
-        show()
-    #-------------------------------------
-    elif sys.argv[1] == 'reply':
-        db = Database()
-        if len(sys.argv) == 2:
-            # no search term. abort
-            exit("Error: notmuch reply requires at least one search term.")
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[2:])
-        msgs = Query(db, querystr).search_messages()
-        print Notmuch().format_reply(msgs)
-    #-------------------------------------
-    elif sys.argv[1] == 'count':
-        if len(sys.argv) == 2:
-            # no further search term, count all
-            querystr = ''
-        else:
-            # mangle arguments wrapping terms with spaces in quotes
-            querystr = quote_query_line(sys.argv[2:])
-       print Database().create_query(querystr).count_messages()
-    #-------------------------------------
-    elif sys.argv[1] == 'tag':
-        # build lists of tags to be added and removed
-        add = []
-        remove = []
-        while not sys.argv[2] == '--' and \
-                (sys.argv[2].startswith('+') or sys.argv[2].startswith('-')):
-                    if sys.argv[2].startswith('+'):
-                        # append to add list without initial +
-                        add.append(sys.argv.pop(2)[1:])
-                    else:
-                        # append to remove list without initial -
-                        remove.append(sys.argv.pop(2)[1:])
-        # skip eventual '--'
-        if sys.argv[2] == '--': sys.argv.pop(2)
-        # the rest is search terms
-        querystr = quote_query_line(sys.argv[2:])
-        db = Database(mode=Database.MODE.READ_WRITE)
-        msgs  = Query(db, querystr).search_messages()
-        for msg in msgs:
-            # actually add and remove all tags
-            map(msg.add_tag, add)
-            map(msg.remove_tag, remove)
-    #-------------------------------------
-    elif sys.argv[1] == 'search-tags':
-        if len(sys.argv) == 2:
-            # no further search term
-            print "\n".join(Database().get_all_tags())
-        else:
-            # mangle arguments wrapping terms with spaces in quotes
-            querystr = quote_query_line(sys.argv[2:])
-            db = Database()
-            msgs  = Query(db, querystr).search_messages()
-            print "\n".join([t for t in msgs.collect_tags()])
-    #-------------------------------------
-    elif sys.argv[1] == 'dump':
-        # TODO: implement "dump <filename>"
-        if len(sys.argv) == 2:
-            f = sys.stdout
-        else:
-            f = open(sys.argv[2], "w")
-        db = Database()
-        query = Query(db, '')
-        query.set_sort(Query.SORT.MESSAGE_ID)
-        msgs = query.search_messages()
-        for msg in msgs:
-            f.write("%s (%s)\n" % (msg.get_message_id(), msg.get_tags()))
-    #-------------------------------------
-    elif sys.argv[1] == 'restore':
-        if len(sys.argv) == 2:
-            print("No filename given. Reading dump from stdin.")
-            f = sys.stdin
-        else:
-            f = open(sys.argv[2], "r")
-
-        # split the msg id and the tags
-        MSGID_TAGS = re.compile("(\S+)\s\((.*)\)$")
-        db = Database(mode=Database.MODE.READ_WRITE)
-
-        #read each line of the dump file
-        for line in f:
-            msgs = MSGID_TAGS.match(line)
-            if not msgs:
-                sys.stderr.write("Warning: Ignoring invalid input line: %s" %
-                        line)
-                continue
-            # split line in components and fetch message
-            msg_id = msgs.group(1)
-            new_tags = set(msgs.group(2).split())
-            msg = db.find_message(msg_id)
-
-            if msg == None:
-                sys.stderr.write(
-                        "Warning: Cannot apply tags to missing message: %s\n" % msg_id)
-                continue
-
-            # do nothing if the old set of tags is the same as the new one
-            old_tags = set(msg.get_tags())
-            if old_tags == new_tags: continue
-
-            # set the new tags
-            msg.freeze()
-            # only remove tags if the new ones are not a superset anyway
-            if not (new_tags > old_tags): msg.remove_all_tags()
-            for tag in new_tags: msg.add_tag(tag)
-            msg.thaw()
-    #-------------------------------------
-    else:
-        # unknown command
-        exit("Error: Unknown command '%s' (see \"notmuch help\")" % sys.argv[1])
-
-def part():
-    db = Database()
-    query_string = ''
-    part_num = 0
-    first_search_term = 0
-    for (num, arg) in enumerate(sys.argv[1:]):
-        if arg.startswith('--part='):
-            part_num_str = arg.split("=")[1]
-            try:
-                part_num = int(part_num_str)
-            except ValueError:
-                # just emulating behavior
-                exit(1)
-        elif not arg.startswith('--'):
-            # save the position of the first sys.argv
-            # that is a search term
-            first_search_term = num + 1
-    if first_search_term:
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[first_search_term:])
-    qry = Query(db,querystr)
-    msgs = [msg for msg in qry.search_messages()]
-
-    if not msgs:
-        sys.exit(1)
-    elif len(msgs) > 1:
-        raise Exception("search term did not match precisely one message")
-    else:
-        msg = msgs[0]
-        print msg.get_part(part_num)
-
-def search():
-    db = Database()
-    query_string = ''
-    sort_order = "newest-first"
-    first_search_term = 0
-    for (num, arg) in enumerate(sys.argv[1:]):
-        if arg.startswith('--sort='):
-            sort_order=arg.split("=")[1]
-            if not sort_order in ("oldest-first", "newest-first"):
-                raise Exception("unknown sort order")
-        elif not arg.startswith('--'):
-            # save the position of the first sys.argv that is a search term
-            first_search_term = num + 1
-
-    if first_search_term:
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[first_search_term:])
-
-    qry = Query(db, querystr)
-    if sort_order == "oldest-first":
-        qry.set_sort(Query.SORT.OLDEST_FIRST)
-    else:
-        qry.set_sort(Query.SORT.NEWEST_FIRST)
-        threads = qry.search_threads()
-
-    for thread in threads:
-        print thread
-
-def show():
-    entire_thread = False
-    db = Database()
-    out_format = "text"
-    querystr = ''
-    first_search_term = None
-
-    # ugly homegrown option parsing
-    # TODO: use OptionParser
-    for (num, arg) in enumerate(sys.argv[1:]):
-        if arg == '--entire-thread':
-            entire_thread = True
-        elif arg.startswith("--format="):
-            out_format = arg.split("=")[1]
-            if out_format == 'json':
-                # for compatibility use --entire-thread for json
-                  entire_thread = True
-            if not out_format in ("json", "text"):
-                  raise Exception("unknown format")
-        elif not arg.startswith('--'):
-            # save the position of the first sys.argv that is a search term
-            first_search_term = num + 1
-
-    if first_search_term:
-        # mangle arguments wrapping terms with spaces in quotes
-        querystr = quote_query_line(sys.argv[first_search_term:])
-
-    threads = Query(db, querystr).search_threads()
-    first_toplevel = True
-    if out_format == "json":
-        sys.stdout.write("[")
-    for thread in threads:
-        msgs = thread.get_toplevel_messages()
-        if not first_toplevel:
-            if out_format == "json":
-                sys.stdout.write(", ")
-        first_toplevel = False
-        msgs.print_messages(out_format, 0, entire_thread)
-
-    if out_format == "json":
-        sys.stdout.write("]")
-    sys.stdout.write("\n")
-
-if __name__ == '__main__':
-    main()
-
-# TODO: implement
-"""
-setup (not?)
-new (halfway there)
-"""
diff --git a/notmuch/__init__.py b/notmuch/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/notmuch/database.py b/notmuch/database.py
new file mode 100644 (file)
index 0000000..f02ec40
--- /dev/null
@@ -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 cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
+from cnotmuch.thread import Threads
+from cnotmuch.message import Messages, Message
+from cnotmuch.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 "<cnotmuch 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/notmuch/globals.py b/notmuch/globals.py
new file mode 100644 (file)
index 0000000..8b0d8d0
--- /dev/null
@@ -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/notmuch/message.py b/notmuch/message.py
new file mode 100644 (file)
index 0000000..f8d7d5e
--- /dev/null
@@ -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 cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
+from cnotmuch.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/notmuch/notmuch.py b/notmuch/notmuch.py
new file mode 100644 (file)
index 0000000..513209d
--- /dev/null
@@ -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 cnotmuch.globals import nmlib, STATUS, NotmuchError
+__LICENSE__="GPL v3+"
+__VERSION__='0.2.2'
+__AUTHOR__ ='Sebastian Spaeth <Sebastian@SSpaeth.de>'
diff --git a/notmuch/tag.py b/notmuch/tag.py
new file mode 100644 (file)
index 0000000..c89e4fe
--- /dev/null
@@ -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 cnotmuch.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/notmuch/thread.py b/notmuch/thread.py
new file mode 100644 (file)
index 0000000..19bef1a
--- /dev/null
@@ -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 cnotmuch.globals import nmlib, STATUS, NotmuchError
+from cnotmuch.message import Messages
+from cnotmuch.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)
index 256fd4e5aee6dfceebb783ebee7b664ad313e87f..7a4b43f5fa5409cb35cbaea69624d2b46bbc9cbb 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -1,20 +1,20 @@
 #!/usr/bin/env python
 
 from distutils.core import setup
-from cnotmuch.notmuch import __VERSION__
-setup(name='cnotmuch',
+from notmuch import __VERSION__
+setup(name='notmuch',
       version=__VERSION__,
       description='Python binding of the notmuch mail search and indexing library.',
       author='Sebastian Spaeth',
       author_email='Sebastian@SSpaeth.de',
       url='http://bitbucket.org/spaetz/cnotmuch/',
       download_url='http://bitbucket.org/spaetz/cnotmuch/get/v'+__VERSION__+'.tar.gz',
-      packages=['cnotmuch'],
+      packages=['notmuch'],
       keywords = ["library", "email"],
       long_description="""Overview
 ==============
 
-The cnotmuch  module provides an interface to the `notmuch <http://notmuchmail.org>`_ functionality, directly interfacing with a shared notmuch library. Notmuch provides a maildatabase that allows for extremely quick searching and filtering of your email according to various criteria.
+The notmuch  module provides an interface to the `notmuch <http://notmuchmail.org>`_ functionality, directly interfacing with a shared notmuch library. Notmuch provides a maildatabase that allows for extremely quick searching and filtering of your email according to various criteria.
 
 The documentation for the latest cnotmuch release can be `viewed online <http://packages.python.org/cnotmuch>`_.