2 from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
4 from cnotmuch.thread import Threads
5 from cnotmuch.message import Messages
6 from cnotmuch.tag import Tags
8 class Database(object):
9 """Represents a notmuch database (wraps notmuch_database_t)
11 .. note:: Do remember that as soon as we tear down this object,
12 all underlying derived objects such as queries, threads,
13 messages, tags etc will be freed by the underlying library
14 as well. Accessing these objects will lead to segfaults and
15 other unexpected behavior. See above for more details.
18 """Class attribute to cache user's default database"""
20 MODE = Enum(['READ_ONLY','READ_WRITE'])
21 """Constants: Mode in which to open the database"""
23 """notmuch_database_get_directory"""
24 _get_directory = nmlib.notmuch_database_get_directory
25 _get_directory.restype = c_void_p
27 """notmuch_database_get_path (notmuch_database_t *database)"""
28 _get_path = nmlib.notmuch_database_get_path
29 _get_path.restype = c_char_p
31 """notmuch_database_get_version"""
32 _get_version = nmlib.notmuch_database_get_version
33 _get_version.restype = c_uint
35 """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
36 _open = nmlib.notmuch_database_open
37 _open.restype = c_void_p
39 """ notmuch_database_find_message """
40 _find_message = nmlib.notmuch_database_find_message
41 _find_message.restype = c_void_p
43 """notmuch_database_get_all_tags (notmuch_database_t *database)"""
44 _get_all_tags = nmlib.notmuch_database_get_all_tags
45 _get_all_tags.restype = c_void_p
47 """ notmuch_database_create(const char *path):"""
48 _create = nmlib.notmuch_database_create
49 _create.restype = c_void_p
51 def __init__(self, path=None, create=False, mode= 0):
52 """If *path* is *None*, we will try to read a users notmuch
53 configuration and use his configured database. The location of the
54 configuration file can be specified through the environment variable
55 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
57 If *create* is `True`, the database will always be created in
58 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
60 :param path: Directory to open/create the database in (see
61 above for behavior if `None`)
62 :type path: `str` or `None`
63 :param create: Pass `False` to open an existing, `True` to create a new
66 :param mode: Mode to open a database in. Is always
67 :attr:`MODE`.READ_WRITE when creating a new one.
68 :type mode: :attr:`MODE`
70 :exception: :exc:`NotmuchError` in case of failure.
74 # no path specified. use a user's default database
75 if Database._std_db_path is None:
76 #the following line throws a NotmuchError if it fails
77 Database._std_db_path = self._get_user_default_db()
78 path = Database._std_db_path
85 def _verify_initialized_db(self):
86 """Raises a NotmuchError in case self._db is still None"""
88 raise NotmuchError(STATUS.NOT_INITIALIZED)
90 def create(self, path):
91 """Creates a new notmuch database
93 This function is used by __init__() and usually does not need
94 to be called directly. It wraps the underlying
95 *notmuch_database_create* function and creates a new notmuch
96 database at *path*. It will always return a database in
97 :attr:`MODE`.READ_WRITE mode as creating an empty database for
98 reading only does not make a great deal of sense.
100 :param path: A directory in which we should create the database.
103 :exception: :exc:`NotmuchError` in case of any failure
104 (after printing an error message on stderr).
106 if self._db is not None:
108 message="Cannot create db, this Database() already has an open one.")
110 res = Database._create(path, Database.MODE.READ_WRITE)
114 message="Could not create the specified database")
117 def open(self, path, mode= 0):
118 """Opens an existing database
120 This function is used by __init__() and usually does not need
121 to be called directly. It wraps the underlying
122 *notmuch_database_open* function.
124 :param status: Open the database in read-only or read-write mode
125 :type status: :attr:`MODE`
127 :exception: Raises :exc:`NotmuchError` in case
128 of any failure (after printing an error message on stderr).
131 res = Database._open(path, mode)
135 message="Could not open the specified database")
139 """Returns the file path of an open database
141 Wraps notmuch_database_get_path"""
142 # Raise a NotmuchError if not initialized
143 self._verify_initialized_db()
145 return Database._get_path(self._db)
147 def get_version(self):
148 """Returns the database format version
150 :returns: The database version as positive integer
151 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
152 the database was not intitialized.
154 # Raise a NotmuchError if not initialized
155 self._verify_initialized_db()
157 return Database._get_version (self._db)
159 def needs_upgrade(self):
160 """Does this database need to be upgraded before writing to it?
162 If this function returns True then no functions that modify the
163 database (:meth:`add_message`, :meth:`add_tag`,
164 :meth:`Directory.set_mtime`, etc.) will work unless :meth:`upgrade`
165 is called successfully first.
167 :returns: `True` or `False`
168 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
169 the database was not intitialized.
171 # Raise a NotmuchError if not initialized
172 self._verify_initialized_db()
174 return notmuch_database_needs_upgrade(self._db)
176 def get_directory(self, path):
177 """Returns a :class:`Directory` of path,
178 (creating it if it does not exist(?))
180 .. warning:: This call needs a writeable database in
181 Database.MODE.READ_WRITE mode. The underlying library will exit the
182 program if this method is used on a read-only database!
184 :param path: A str containing the path relative to the path of database
185 (see :meth:`get_path`), or else should be an absolute path
186 with initial components that match the path of 'database'.
188 :returns: :class:`Directory` or raises an exception.
189 :exception: :exc:`NotmuchError`
191 STATUS.NOT_INITIALIZED
192 If the database was not intitialized.
195 If path is not relative database or absolute with initial
196 components same as database.
199 # Raise a NotmuchError if not initialized
200 self._verify_initialized_db()
202 # sanity checking if path is valid, and make path absolute
203 if path[0] == os.sep:
204 # we got an absolute path
205 if not path.startswith(self.get_path()):
206 # but its initial components are not equal to the db path
207 raise NotmuchError(STATUS.FILE_ERROR,
208 message="Database().get_directory() called with a wrong absolute path.")
211 #we got a relative path, make it absolute
212 abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path))
214 dir_p = Database._get_directory(self._db, path);
216 # return the Directory, init it with the absolute path
217 return Directory(abs_dirpath, dir_p, self)
219 def add_message(self, filename):
220 """Adds a new message to the database
222 `filename` should be a path relative to the path of the open
223 database (see :meth:`get_path`), or else should be an absolute
224 filename with initial components that match the path of the
227 The file should be a single mail message (not a multi-message mbox)
228 that is expected to remain at its current location, since the
229 notmuch database will reference the filename, and will not copy the
230 entire contents of the file.
232 :returns: On success, we return
234 1) a :class:`Message` object that can be used for things
235 such as adding tags to the just-added message.
236 2) one of the following STATUS values:
239 Message successfully added to database.
240 STATUS.DUPLICATE_MESSAGE_ID
241 Message has the same message ID as another message already
242 in the database. The new filename was successfully added
243 to the message in the database.
245 :rtype: 2-tuple(:class:`Message`, STATUS)
247 :exception: Raises a :exc:`NotmuchError` with the following meaning.
248 If such an exception occurs, nothing was added to the database.
251 An error occurred trying to open the file, (such as
252 permission denied, or file not found, etc.).
253 STATUS.FILE_NOT_EMAIL
254 The contents of filename don't look like an email message.
255 STATUS.READ_ONLY_DATABASE
256 Database was opened in read-only mode so no message can
258 STATUS.NOT_INITIALIZED
259 The database has not been initialized.
261 # Raise a NotmuchError if not initialized
262 self._verify_initialized_db()
265 status = nmlib.notmuch_database_add_message(self._db,
269 if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
270 raise NotmuchError(status)
272 #construct Message() and return
273 msg = Message(msg_p, self)
276 def remove_message(self, filename):
277 """Removes a message from the given notmuch database
279 Note that only this particular filename association is removed from
280 the database. If the same message (as determined by the message ID)
281 is still available via other filenames, then the message will
282 persist in the database for those filenames. When the last filename
283 is removed for a particular message, the database content for that
284 message will be entirely removed.
286 :returns: A STATUS.* value with the following meaning:
289 The last filename was removed and the message was removed
291 STATUS.DUPLICATE_MESSAGE_ID
292 This filename was removed but the message persists in the
293 database with at least one other filename.
295 :exception: Raises a :exc:`NotmuchError` with the following meaning.
296 If such an exception occurs, nothing was removed from the database.
298 STATUS.READ_ONLY_DATABASE
299 Database was opened in read-only mode so no message can be
301 STATUS.NOT_INITIALIZED
302 The database has not been initialized.
304 # Raise a NotmuchError if not initialized
305 self._verify_initialized_db()
307 status = nmlib.notmuch_database_remove_message(self._db,
310 def find_message(self, msgid):
311 """Returns a :class:`Message` as identified by its message ID
313 Wraps the underlying *notmuch_database_find_message* function.
315 :param msgid: The message ID
317 :returns: :class:`Message` or `None` if no message is found or if an
318 out-of-memory situation occurs.
319 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
320 the database was not intitialized.
322 # Raise a NotmuchError if not initialized
323 self._verify_initialized_db()
325 msg_p = Database._find_message(self._db, msgid)
328 return Message(msg_p, self)
330 def get_all_tags(self):
331 """Returns :class:`Tags` with a list of all tags found in the database
333 :returns: :class:`Tags`
334 :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
336 # Raise a NotmuchError if not initialized
337 self._verify_initialized_db()
339 tags_p = Database._get_all_tags (self._db)
341 raise NotmuchError(STATUS.NULL_POINTER)
342 return Tags(tags_p, self)
344 def create_query(self, querystring):
345 """Returns a :class:`Query` derived from this database
347 This is a shorthand method for doing::
350 # Automatically frees the Database() when 'q' is deleted
352 q = Database(dbpath).create_query('from:"Biene Maja"')
354 # long version, which is functionally equivalent but will keep the
355 # Database in the 'db' variable around after we delete 'q':
357 db = Database(dbpath)
358 q = Query(db,'from:"Biene Maja"')
360 This function is a python extension and not in the underlying C API.
362 # Raise a NotmuchError if not initialized
363 self._verify_initialized_db()
365 return Query(self, querystring)
368 return "'Notmuch DB " + self.get_path() + "'"
371 """Close and free the notmuch database if needed"""
372 if self._db is not None:
373 nmlib.notmuch_database_close(self._db)
375 def _get_user_default_db(self):
376 """ Reads a user's notmuch config and returns his db location
378 Throws a NotmuchError if it cannot find it"""
379 from ConfigParser import SafeConfigParser
380 config = SafeConfigParser()
381 conf_f = os.getenv('NOTMUCH_CONFIG',
382 os.path.expanduser('~/.notmuch-config'))
384 if not config.has_option('database','path'):
385 raise NotmuchError(message=
386 "No DB path specified and no user default found")
387 return config.get('database','path')
391 """Property returning a pointer to `notmuch_database_t` or `None`
393 This should normally not be needed by a user (and is not yet
394 guaranteed to remain stable in future versions).
398 #------------------------------------------------------------------------------
400 """Represents a search query on an opened :class:`Database`.
402 A query selects and filters a subset of messages from the notmuch
403 database we derive from.
405 Query() provides an instance attribute :attr:`sort`, which
406 contains the sort order (if specified via :meth:`set_sort`) or
409 Technically, it wraps the underlying *notmuch_query_t* struct.
411 .. note:: Do remember that as soon as we tear down this object,
412 all underlying derived objects such as threads,
413 messages, tags etc will be freed by the underlying library
414 as well. Accessing these objects will lead to segfaults and
415 other unexpected behavior. See above for more details.
418 SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
419 """Constants: Sort order in which to return results"""
421 """notmuch_query_create"""
422 _create = nmlib.notmuch_query_create
423 _create.restype = c_void_p
425 """notmuch_query_search_threads"""
426 _search_threads = nmlib.notmuch_query_search_threads
427 _search_threads.restype = c_void_p
429 """notmuch_query_search_messages"""
430 _search_messages = nmlib.notmuch_query_search_messages
431 _search_messages.restype = c_void_p
434 """notmuch_query_count_messages"""
435 _count_messages = nmlib.notmuch_query_count_messages
436 _count_messages.restype = c_uint
438 def __init__(self, db, querystr):
440 :param db: An open database which we derive the Query from.
441 :type db: :class:`Database`
442 :param querystr: The query string for the message.
448 self.create(db, querystr)
450 def create(self, db, querystr):
451 """Creates a new query derived from a Database
453 This function is utilized by __init__() and usually does not need to
456 :param db: Database to create the query from.
457 :type db: :class:`Database`
458 :param querystr: The query string
461 :exception: :exc:`NotmuchError`
463 * STATUS.NOT_INITIALIZED if db is not inited
464 * STATUS.NULL_POINTER if the query creation failed
468 raise NotmuchError(STATUS.NOT_INITIALIZED)
469 # create reference to parent db to keep it alive
472 # create query, return None if too little mem available
473 query_p = Query._create(db.db_p, querystr)
475 NotmuchError(STATUS.NULL_POINTER)
476 self._query = query_p
478 def set_sort(self, sort):
479 """Set the sort order future results will be delivered in
481 Wraps the underlying *notmuch_query_set_sort* function.
483 :param sort: Sort order (see :attr:`Query.SORT`)
485 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
488 if self._query is None:
489 raise NotmuchError(STATUS.NOT_INITIALIZED)
492 nmlib.notmuch_query_set_sort(self._query, sort)
494 def search_threads(self):
495 """Execute a query for threads
497 Execute a query for threads, returning a :class:`Threads` iterator.
498 The returned threads are owned by the query and as such, will only be
499 valid until the Query is deleted.
501 The method sets :attr:`Message.FLAG`\.MATCH for those messages that
502 match the query. The method :meth:`Message.get_flag` allows us
503 to get the value of this flag.
505 Technically, it wraps the underlying
506 *notmuch_query_search_threads* function.
508 :returns: :class:`Threads`
509 :exception: :exc:`NotmuchError`
511 * STATUS.NOT_INITIALIZED if query is not inited
512 * STATUS.NULL_POINTER if search_threads failed
514 if self._query is None:
515 raise NotmuchError(STATUS.NOT_INITIALIZED)
517 threads_p = Query._search_threads(self._query)
519 if threads_p is None:
520 NotmuchError(STATUS.NULL_POINTER)
522 return Threads(threads_p,self)
524 def search_messages(self):
525 """Filter messages according to the query and return
526 :class:`Messages` in the defined sort order
528 Technically, it wraps the underlying
529 *notmuch_query_search_messages* function.
531 :returns: :class:`Messages`
532 :exception: :exc:`NotmuchError`
534 * STATUS.NOT_INITIALIZED if query is not inited
535 * STATUS.NULL_POINTER if search_messages failed
537 if self._query is None:
538 raise NotmuchError(STATUS.NOT_INITIALIZED)
540 msgs_p = Query._search_messages(self._query)
543 NotmuchError(STATUS.NULL_POINTER)
545 return Messages(msgs_p,self)
547 def count_messages(self):
548 """Estimate the number of messages matching the query
550 This function performs a search and returns Xapian's best
551 guess as to the number of matching messages. It is much faster
552 than performing :meth:`search_messages` and counting the
553 result with `len()` (although it always returned the same
554 result in my tests). Technically, it wraps the underlying
555 *notmuch_query_count_messages* function.
557 :returns: :class:`Messages`
558 :exception: :exc:`NotmuchError`
560 * STATUS.NOT_INITIALIZED if query is not inited
562 if self._query is None:
563 raise NotmuchError(STATUS.NOT_INITIALIZED)
565 return Query._count_messages(self._query)
568 """Close and free the Query"""
569 if self._query is not None:
570 nmlib.notmuch_query_destroy (self._query)
573 #------------------------------------------------------------------------------
574 class Directory(object):
575 """Represents a directory entry in the notmuch directory
577 Modifying attributes of this object will modify the
578 database, not the real directory attributes.
580 The Directory object is usually derived from another object
581 e.g. via :meth:`Database.get_directory`, and will automatically be
582 become invalid whenever that parent is deleted. You should
583 therefore initialized this object handing it a reference to the
584 parent, preventing the parent from automatically being garbage
588 """notmuch_directory_get_mtime"""
589 _get_mtime = nmlib.notmuch_directory_get_mtime
590 _get_mtime.restype = c_long
592 """notmuch_directory_set_mtime"""
593 _set_mtime = nmlib.notmuch_directory_set_mtime
594 _set_mtime.argtypes = [c_char_p, c_long]
596 """notmuch_directory_get_child_directories"""
597 _get_child_directories = nmlib.notmuch_directory_get_child_directories
598 _get_child_directories.restype = c_void_p
600 def _verify_dir_initialized(self):
601 """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None"""
602 if self._dir_p is None:
603 raise NotmuchError(STATUS.NOT_INITIALIZED)
605 def __init__(self, path, dir_p, parent):
607 :param path: The absolute path of the directory object.
608 :param dir_p: The pointer to an internal notmuch_directory_t object.
609 :param parent: The object this Directory is derived from
610 (usually a Database()). We do not directly use
611 this, but store a reference to it as long as
612 this Directory object lives. This keeps the
615 #TODO, sanity checking that the path is really absolute?
618 self._parent = parent
621 def set_mtime (self, mtime):
622 """Sets the mtime value of this directory in the database
624 The intention is for the caller to use the mtime to allow efficient
625 identification of new messages to be added to the database. The
626 recommended usage is as follows:
628 * Read the mtime of a directory from the filesystem
630 * Call :meth:`Database.add_message` for all mail files in
633 * Call notmuch_directory_set_mtime with the mtime read from the
634 filesystem. Then, when wanting to check for updates to the
635 directory in the future, the client can call :meth:`get_mtime`
636 and know that it only needs to add files if the mtime of the
637 directory and files are newer than the stored timestamp.
639 .. note:: :meth:`get_mtime` function does not allow the caller
640 to distinguish a timestamp of 0 from a non-existent
641 timestamp. So don't store a timestamp of 0 unless you are
642 comfortable with that.
644 :param mtime: A (time_t) timestamp
645 :returns: Nothing on success, raising an exception on failure.
646 :exception: :exc:`NotmuchError`:
648 STATUS.XAPIAN_EXCEPTION
649 A Xapian exception occurred, mtime not stored.
650 STATUS.READ_ONLY_DATABASE
651 Database was opened in read-only mode so directory
652 mtime cannot be modified.
653 STATUS.NOT_INITIALIZED
654 The directory has not been initialized
656 #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
657 self._verify_dir_initialized()
659 #TODO: make sure, we convert the mtime parameter to a 'c_long'
660 status = Directory._set_mtime(self._dir_p, mtime)
663 if status == STATUS.SUCCESS:
665 #fail with Exception otherwise
666 raise NotmuchError(status)
668 def get_mtime (self):
669 """Gets the mtime value of this directory in the database
671 Retrieves a previously stored mtime for this directory.
673 :param mtime: A (time_t) timestamp
674 :returns: Nothing on success, raising an exception on failure.
675 :exception: :exc:`NotmuchError`:
677 STATUS.NOT_INITIALIZED
678 The directory has not been initialized
680 #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None
681 self._verify_dir_initialized()
683 return Directory._get_mtime (self._dir_p)
686 # Make mtime attribute a property of Directory()
687 mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
688 and setting of the Directory *mtime*""")
690 def get_child_files(self):
691 """Gets a Filenames iterator listing all the filenames of
692 messages in the database within the given directory.
694 The returned filenames will be the basename-entries only (not
698 #notmuch_filenames_t * notmuch_directory_get_child_files (notmuch_directory_t *directory);
700 def get_child_directories(self):
701 """Gets a Filenams iterator listing all the filenames of
702 sub-directories in the database within the given directory
704 The returned filenames will be the basename-entries only (not
707 #notmuch_filenames_t * notmuch_directory_get_child_directories (notmuch_directory_t *directory);
712 """Returns the absolute path of this Directory"""
716 """Object representation"""
717 return "<cnotmuch Directory object '%s'>" % self._path
720 """Close and free the Directory"""
721 if self._dir_p is not None:
722 nmlib.notmuch_directory_destroy(self._dir_p)