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
+++ /dev/null
-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)
+++ /dev/null
-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])
-
+++ /dev/null
-# 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)
+++ /dev/null
-"""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>'
+++ /dev/null
-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)
+++ /dev/null
-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)
+++ /dev/null
-#!/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)
-"""
--- /dev/null
+"""
+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)
--- /dev/null
+"""
+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])
+
--- /dev/null
+"""
+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)
--- /dev/null
+"""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>'
--- /dev/null
+"""
+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)
--- /dev/null
+"""
+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)
#!/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>`_.