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"""
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"""
36 _open = nmlib.notmuch_database_open
37 _open.restype = c_void_p
39 """notmuch_database_upgrade"""
40 _upgrade = nmlib.notmuch_database_upgrade
41 _upgrade.argtypes = [c_void_p, c_void_p, c_void_p]
43 """ notmuch_database_find_message"""
44 _find_message = nmlib.notmuch_database_find_message
45 _find_message.restype = c_void_p
47 """notmuch_database_get_all_tags"""
48 _get_all_tags = nmlib.notmuch_database_get_all_tags
49 _get_all_tags.restype = c_void_p
51 """notmuch_database_create"""
52 _create = nmlib.notmuch_database_create
53 _create.restype = c_void_p
55 def __init__(self, path=None, create=False, mode= 0):
56 """If *path* is `None`, we will try to read a users notmuch
57 configuration and use his configured database. The location of the
58 configuration file can be specified through the environment variable
59 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
61 If *create* is `True`, the database will always be created in
62 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
64 :param path: Directory to open/create the database in (see
65 above for behavior if `None`)
66 :type path: `str` or `None`
67 :param create: Pass `False` to open an existing, `True` to create a new
70 :param mode: Mode to open a database in. Is always
71 :attr:`MODE`.READ_WRITE when creating a new one.
72 :type mode: :attr:`MODE`
74 :exception: :exc:`NotmuchError` in case of failure.
78 # no path specified. use a user's default database
79 if Database._std_db_path is None:
80 #the following line throws a NotmuchError if it fails
81 Database._std_db_path = self._get_user_default_db()
82 path = Database._std_db_path
89 def _verify_initialized_db(self):
90 """Raises a NotmuchError in case self._db is still None"""
92 raise NotmuchError(STATUS.NOT_INITIALIZED)
94 def create(self, path):
95 """Creates a new notmuch database
97 This function is used by __init__() and usually does not need
98 to be called directly. It wraps the underlying
99 *notmuch_database_create* function and creates a new notmuch
100 database at *path*. It will always return a database in :attr:`MODE`
101 .READ_WRITE mode as creating an empty database for
102 reading only does not make a great deal of sense.
104 :param path: A directory in which we should create the database.
107 :exception: :exc:`NotmuchError` in case of any failure
108 (after printing an error message on stderr).
110 if self._db is not None:
112 message="Cannot create db, this Database() already has an open one.")
114 res = Database._create(path, Database.MODE.READ_WRITE)
118 message="Could not create the specified database")
121 def open(self, path, mode= 0):
122 """Opens an existing database
124 This function is used by __init__() and usually does not need
125 to be called directly. It wraps the underlying
126 *notmuch_database_open* function.
128 :param status: Open the database in read-only or read-write mode
129 :type status: :attr:`MODE`
131 :exception: Raises :exc:`NotmuchError` in case
132 of any failure (after printing an error message on stderr).
135 res = Database._open(path, mode)
139 message="Could not open the specified database")
143 """Returns the file path of an open database
145 Wraps *notmuch_database_get_path*."""
146 # Raise a NotmuchError if not initialized
147 self._verify_initialized_db()
149 return Database._get_path(self._db)
151 def get_version(self):
152 """Returns the database format version
154 :returns: The database version as positive integer
155 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
156 the database was not intitialized.
158 # Raise a NotmuchError if not initialized
159 self._verify_initialized_db()
161 return Database._get_version (self._db)
163 def needs_upgrade(self):
164 """Does this database need to be upgraded before writing to it?
166 If this function returns `True` then no functions that modify the
167 database (:meth:`add_message`,
168 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
169 etc.) will work unless :meth:`upgrade` is called successfully first.
171 :returns: `True` or `False`
172 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
173 the database was not intitialized.
175 # Raise a NotmuchError if not initialized
176 self._verify_initialized_db()
178 return notmuch_database_needs_upgrade(self._db)
181 """Upgrades the current database
183 After opening a database in read-write mode, the client should
184 check if an upgrade is needed (notmuch_database_needs_upgrade) and
185 if so, upgrade with this function before making any modifications.
187 NOT IMPLEMENTED: The optional progress_notify callback can be
188 used by the caller to provide progress indication to the
189 user. If non-NULL it will be called periodically with
190 'progress' as a floating-point value in the range of [0.0..1.0]
191 indicating the progress made so far in the upgrade process.
193 :TODO: catch exceptions, document return values and etc...
195 # Raise a NotmuchError if not initialized
196 self._verify_initialized_db()
198 status = Database._upgrade (self._db, None, None)
199 #TODO: catch exceptions, document return values and etc
202 def get_directory(self, path):
203 """Returns a :class:`Directory` of path,
204 (creating it if it does not exist(?))
206 .. warning:: This call needs a writeable database in
207 Database.MODE.READ_WRITE mode. The underlying library will exit the
208 program if this method is used on a read-only database!
210 :param path: A str containing the path relative to the path of database
211 (see :meth:`get_path`), or else should be an absolute path
212 with initial components that match the path of 'database'.
213 :returns: :class:`Directory` or raises an exception.
214 :exception: :exc:`NotmuchError`
216 STATUS.NOT_INITIALIZED
217 If the database was not intitialized.
220 If path is not relative database or absolute with initial
221 components same as database.
224 # Raise a NotmuchError if not initialized
225 self._verify_initialized_db()
227 # sanity checking if path is valid, and make path absolute
228 if path[0] == os.sep:
229 # we got an absolute path
230 if not path.startswith(self.get_path()):
231 # but its initial components are not equal to the db path
232 raise NotmuchError(STATUS.FILE_ERROR,
233 message="Database().get_directory() called with a wrong absolute path.")
236 #we got a relative path, make it absolute
237 abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path))
239 dir_p = Database._get_directory(self._db, path);
241 # return the Directory, init it with the absolute path
242 return Directory(abs_dirpath, dir_p, self)
244 def add_message(self, filename):
245 """Adds a new message to the database
247 `filename` should be a path relative to the path of the open
248 database (see :meth:`get_path`), or else should be an absolute
249 filename with initial components that match the path of the
252 The file should be a single mail message (not a multi-message mbox)
253 that is expected to remain at its current location, since the
254 notmuch database will reference the filename, and will not copy the
255 entire contents of the file.
257 :returns: On success, we return
259 1) a :class:`Message` object that can be used for things
260 such as adding tags to the just-added message.
261 2) one of the following STATUS values:
264 Message successfully added to database.
265 STATUS.DUPLICATE_MESSAGE_ID
266 Message has the same message ID as another message already
267 in the database. The new filename was successfully added
268 to the message in the database.
270 :rtype: 2-tuple(:class:`Message`, STATUS)
272 :exception: Raises a :exc:`NotmuchError` with the following meaning.
273 If such an exception occurs, nothing was added to the database.
276 An error occurred trying to open the file, (such as
277 permission denied, or file not found, etc.).
278 STATUS.FILE_NOT_EMAIL
279 The contents of filename don't look like an email message.
280 STATUS.READ_ONLY_DATABASE
281 Database was opened in read-only mode so no message can
283 STATUS.NOT_INITIALIZED
284 The database has not been initialized.
286 # Raise a NotmuchError if not initialized
287 self._verify_initialized_db()
290 status = nmlib.notmuch_database_add_message(self._db,
294 if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
295 raise NotmuchError(status)
297 #construct Message() and return
298 msg = Message(msg_p, self)
301 def remove_message(self, filename):
302 """Removes a message from the given notmuch database
304 Note that only this particular filename association is removed from
305 the database. If the same message (as determined by the message ID)
306 is still available via other filenames, then the message will
307 persist in the database for those filenames. When the last filename
308 is removed for a particular message, the database content for that
309 message will be entirely removed.
311 :returns: A STATUS value with the following meaning:
314 The last filename was removed and the message was removed
316 STATUS.DUPLICATE_MESSAGE_ID
317 This filename was removed but the message persists in the
318 database with at least one other filename.
320 :exception: Raises a :exc:`NotmuchError` with the following meaning.
321 If such an exception occurs, nothing was removed from the database.
323 STATUS.READ_ONLY_DATABASE
324 Database was opened in read-only mode so no message can be
326 STATUS.NOT_INITIALIZED
327 The database has not been initialized.
329 # Raise a NotmuchError if not initialized
330 self._verify_initialized_db()
332 status = nmlib.notmuch_database_remove_message(self._db,
335 def find_message(self, msgid):
336 """Returns a :class:`Message` as identified by its message ID
338 Wraps the underlying *notmuch_database_find_message* function.
340 :param msgid: The message ID
342 :returns: :class:`Message` or `None` if no message is found or if an
343 out-of-memory situation occurs.
344 :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
345 the database was not intitialized.
347 # Raise a NotmuchError if not initialized
348 self._verify_initialized_db()
350 msg_p = Database._find_message(self._db, msgid)
353 return Message(msg_p, self)
355 def get_all_tags(self):
356 """Returns :class:`Tags` with a list of all tags found in the database
358 :returns: :class:`Tags`
359 :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
361 # Raise a NotmuchError if not initialized
362 self._verify_initialized_db()
364 tags_p = Database._get_all_tags (self._db)
366 raise NotmuchError(STATUS.NULL_POINTER)
367 return Tags(tags_p, self)
369 def create_query(self, querystring):
370 """Returns a :class:`Query` derived from this database
372 This is a shorthand method for doing::
375 # Automatically frees the Database() when 'q' is deleted
377 q = Database(dbpath).create_query('from:"Biene Maja"')
379 # long version, which is functionally equivalent but will keep the
380 # Database in the 'db' variable around after we delete 'q':
382 db = Database(dbpath)
383 q = Query(db,'from:"Biene Maja"')
385 This function is a python extension and not in the underlying C API.
387 # Raise a NotmuchError if not initialized
388 self._verify_initialized_db()
390 return Query(self, querystring)
393 return "'Notmuch DB " + self.get_path() + "'"
396 """Close and free the notmuch database if needed"""
397 if self._db is not None:
398 nmlib.notmuch_database_close(self._db)
400 def _get_user_default_db(self):
401 """ Reads a user's notmuch config and returns his db location
403 Throws a NotmuchError if it cannot find it"""
404 from ConfigParser import SafeConfigParser
405 config = SafeConfigParser()
406 conf_f = os.getenv('NOTMUCH_CONFIG',
407 os.path.expanduser('~/.notmuch-config'))
409 if not config.has_option('database','path'):
410 raise NotmuchError(message=
411 "No DB path specified and no user default found")
412 return config.get('database','path')
416 """Property returning a pointer to `notmuch_database_t` or `None`
418 This should normally not be needed by a user (and is not yet
419 guaranteed to remain stable in future versions).
423 #------------------------------------------------------------------------------
425 """Represents a search query on an opened :class:`Database`.
427 A query selects and filters a subset of messages from the notmuch
428 database we derive from.
430 Query() provides an instance attribute :attr:`sort`, which
431 contains the sort order (if specified via :meth:`set_sort`) or
434 Technically, it wraps the underlying *notmuch_query_t* struct.
436 .. note:: Do remember that as soon as we tear down this object,
437 all underlying derived objects such as threads,
438 messages, tags etc will be freed by the underlying library
439 as well. Accessing these objects will lead to segfaults and
440 other unexpected behavior. See above for more details.
443 SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID'])
444 """Constants: Sort order in which to return results"""
446 """notmuch_query_create"""
447 _create = nmlib.notmuch_query_create
448 _create.restype = c_void_p
450 """notmuch_query_search_threads"""
451 _search_threads = nmlib.notmuch_query_search_threads
452 _search_threads.restype = c_void_p
454 """notmuch_query_search_messages"""
455 _search_messages = nmlib.notmuch_query_search_messages
456 _search_messages.restype = c_void_p
459 """notmuch_query_count_messages"""
460 _count_messages = nmlib.notmuch_query_count_messages
461 _count_messages.restype = c_uint
463 def __init__(self, db, querystr):
465 :param db: An open database which we derive the Query from.
466 :type db: :class:`Database`
467 :param querystr: The query string for the message.
473 self.create(db, querystr)
475 def create(self, db, querystr):
476 """Creates a new query derived from a Database
478 This function is utilized by __init__() and usually does not need to
481 :param db: Database to create the query from.
482 :type db: :class:`Database`
483 :param querystr: The query string
486 :exception: :exc:`NotmuchError`
488 * STATUS.NOT_INITIALIZED if db is not inited
489 * STATUS.NULL_POINTER if the query creation failed
493 raise NotmuchError(STATUS.NOT_INITIALIZED)
494 # create reference to parent db to keep it alive
497 # create query, return None if too little mem available
498 query_p = Query._create(db.db_p, querystr)
500 NotmuchError(STATUS.NULL_POINTER)
501 self._query = query_p
503 def set_sort(self, sort):
504 """Set the sort order future results will be delivered in
506 Wraps the underlying *notmuch_query_set_sort* function.
508 :param sort: Sort order (see :attr:`Query.SORT`)
510 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
513 if self._query is None:
514 raise NotmuchError(STATUS.NOT_INITIALIZED)
517 nmlib.notmuch_query_set_sort(self._query, sort)
519 def search_threads(self):
520 """Execute a query for threads
522 Execute a query for threads, returning a :class:`Threads` iterator.
523 The returned threads are owned by the query and as such, will only be
524 valid until the Query is deleted.
526 The method sets :attr:`Message.FLAG`\.MATCH for those messages that
527 match the query. The method :meth:`Message.get_flag` allows us
528 to get the value of this flag.
530 Technically, it wraps the underlying
531 *notmuch_query_search_threads* function.
533 :returns: :class:`Threads`
534 :exception: :exc:`NotmuchError`
536 * STATUS.NOT_INITIALIZED if query is not inited
537 * STATUS.NULL_POINTER if search_threads failed
539 if self._query is None:
540 raise NotmuchError(STATUS.NOT_INITIALIZED)
542 threads_p = Query._search_threads(self._query)
544 if threads_p is None:
545 NotmuchError(STATUS.NULL_POINTER)
547 return Threads(threads_p,self)
549 def search_messages(self):
550 """Filter messages according to the query and return
551 :class:`Messages` in the defined sort order
553 Technically, it wraps the underlying
554 *notmuch_query_search_messages* function.
556 :returns: :class:`Messages`
557 :exception: :exc:`NotmuchError`
559 * STATUS.NOT_INITIALIZED if query is not inited
560 * STATUS.NULL_POINTER if search_messages failed
562 if self._query is None:
563 raise NotmuchError(STATUS.NOT_INITIALIZED)
565 msgs_p = Query._search_messages(self._query)
568 NotmuchError(STATUS.NULL_POINTER)
570 return Messages(msgs_p,self)
572 def count_messages(self):
573 """Estimate the number of messages matching the query
575 This function performs a search and returns Xapian's best
576 guess as to the number of matching messages. It is much faster
577 than performing :meth:`search_messages` and counting the
578 result with `len()` (although it always returned the same
579 result in my tests). Technically, it wraps the underlying
580 *notmuch_query_count_messages* function.
582 :returns: :class:`Messages`
583 :exception: :exc:`NotmuchError`
585 * STATUS.NOT_INITIALIZED if query is not inited
587 if self._query is None:
588 raise NotmuchError(STATUS.NOT_INITIALIZED)
590 return Query._count_messages(self._query)
593 """Close and free the Query"""
594 if self._query is not None:
595 nmlib.notmuch_query_destroy (self._query)
598 #------------------------------------------------------------------------------
599 class Directory(object):
600 """Represents a directory entry in the notmuch directory
602 Modifying attributes of this object will modify the
603 database, not the real directory attributes.
605 The Directory object is usually derived from another object
606 e.g. via :meth:`Database.get_directory`, and will automatically be
607 become invalid whenever that parent is deleted. You should
608 therefore initialized this object handing it a reference to the
609 parent, preventing the parent from automatically being garbage
613 """notmuch_directory_get_mtime"""
614 _get_mtime = nmlib.notmuch_directory_get_mtime
615 _get_mtime.restype = c_long
617 """notmuch_directory_set_mtime"""
618 _set_mtime = nmlib.notmuch_directory_set_mtime
619 _set_mtime.argtypes = [c_char_p, c_long]
621 """notmuch_directory_get_child_files"""
622 _get_child_files = nmlib.notmuch_directory_get_child_files
623 _get_child_files.restype = c_void_p
625 """notmuch_directory_get_child_directories"""
626 _get_child_directories = nmlib.notmuch_directory_get_child_directories
627 _get_child_directories.restype = c_void_p
629 def _verify_dir_initialized(self):
630 """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None"""
631 if self._dir_p is None:
632 raise NotmuchError(STATUS.NOT_INITIALIZED)
634 def __init__(self, path, dir_p, parent):
636 :param path: The absolute path of the directory object.
637 :param dir_p: The pointer to an internal notmuch_directory_t object.
638 :param parent: The object this Directory is derived from
639 (usually a :class:`Database`). We do not directly use
640 this, but store a reference to it as long as
641 this Directory object lives. This keeps the
646 self._parent = parent
649 def set_mtime (self, mtime):
650 """Sets the mtime value of this directory in the database
652 The intention is for the caller to use the mtime to allow efficient
653 identification of new messages to be added to the database. The
654 recommended usage is as follows:
656 * Read the mtime of a directory from the filesystem
658 * Call :meth:`Database.add_message` for all mail files in
661 * Call notmuch_directory_set_mtime with the mtime read from the
662 filesystem. Then, when wanting to check for updates to the
663 directory in the future, the client can call :meth:`get_mtime`
664 and know that it only needs to add files if the mtime of the
665 directory and files are newer than the stored timestamp.
667 .. note:: :meth:`get_mtime` function does not allow the caller
668 to distinguish a timestamp of 0 from a non-existent
669 timestamp. So don't store a timestamp of 0 unless you are
670 comfortable with that.
672 :param mtime: A (time_t) timestamp
673 :returns: Nothing on success, raising an exception on failure.
674 :exception: :exc:`NotmuchError`:
676 STATUS.XAPIAN_EXCEPTION
677 A Xapian exception occurred, mtime not stored.
678 STATUS.READ_ONLY_DATABASE
679 Database was opened in read-only mode so directory
680 mtime cannot be modified.
681 STATUS.NOT_INITIALIZED
682 The directory has not been initialized
684 #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
685 self._verify_dir_initialized()
687 #TODO: make sure, we convert the mtime parameter to a 'c_long'
688 status = Directory._set_mtime(self._dir_p, mtime)
691 if status == STATUS.SUCCESS:
693 #fail with Exception otherwise
694 raise NotmuchError(status)
696 def get_mtime (self):
697 """Gets the mtime value of this directory in the database
699 Retrieves a previously stored mtime for this directory.
701 :param mtime: A (time_t) timestamp
702 :returns: Nothing on success, raising an exception on failure.
703 :exception: :exc:`NotmuchError`:
705 STATUS.NOT_INITIALIZED
706 The directory has not been initialized
708 #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None
709 self._verify_dir_initialized()
711 return Directory._get_mtime (self._dir_p)
713 # Make mtime attribute a property of Directory()
714 mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
715 and setting of the Directory *mtime* (read-write)
717 See :meth:`get_mtime` and :meth:`set_mtime` for usage and
718 possible exceptions.""")
720 def get_child_files(self):
721 """Gets a Filenames iterator listing all the filenames of
722 messages in the database within the given directory.
724 The returned filenames will be the basename-entries only (not
727 #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
728 self._verify_dir_initialized()
730 files_p = Directory._get_child_files(self._dir_p)
731 return Filenames(files_p, self)
733 def get_child_directories(self):
734 """Gets a :class:`Filenames` iterator listing all the filenames of
735 sub-directories in the database within the given directory
737 The returned filenames will be the basename-entries only (not
740 #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
741 self._verify_dir_initialized()
743 files_p = Directory._get_child_directories(self._dir_p)
744 return Filenames(files_p, self)
748 """Returns the absolute path of this Directory (read-only)"""
752 """Object representation"""
753 return "<cnotmuch Directory object '%s'>" % self._path
756 """Close and free the Directory"""
757 if self._dir_p is not None:
758 nmlib.notmuch_directory_destroy(self._dir_p)
760 #------------------------------------------------------------------------------
761 class Filenames(object):
762 """An iterator over File- or Directory names that are stored in the database
765 #notmuch_filenames_get
766 _get = nmlib.notmuch_filenames_get
767 _get.restype = c_char_p
769 def __init__(self, files_p, parent):
771 :param files_p: The pointer to an internal notmuch_filenames_t object.
772 :param parent: The object this Directory is derived from
773 (usually a Directory()). We do not directly use
774 this, but store a reference to it as long as
775 this Directory object lives. This keeps the
778 self._files_p = files_p
779 self._parent = parent
782 """ Make Filenames an iterator """
786 if self._files_p is None:
787 raise NotmuchError(STATUS.NOT_INITIALIZED)
789 if not nmlib.notmuch_filenames_valid(self._files_p):
793 file = Filenames._get (self._files_p)
794 nmlib.notmuch_filenames_move_to_next(self._files_p)
798 """len(:class:`Filenames`) returns the number of contained files
800 .. note:: As this iterates over the files, we will not be able to
801 iterate over them again! So this will fail::
804 files = Database().get_directory('').get_child_files()
805 if len(files) > 0: #this 'exhausts' msgs
806 # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
807 for file in files: print file
809 if self._files_p is None:
810 raise NotmuchError(STATUS.NOT_INITIALIZED)
813 while nmlib.notmuch_filenames_valid(self._files_p):
814 nmlib.notmuch_filenames_move_to_next(self._files_p)
820 """Close and free Filenames"""
821 if self._files_p is not None:
822 nmlib.notmuch_filenames_destroy(self._files_p)