2 This file is part of notmuch.
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 You should have received a copy of the GNU General Public License
15 along with notmuch. If not, see <https://www.gnu.org/licenses/>.
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
23 from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER
24 from .compat import SafeConfigParser
25 from .globals import (
42 from .message import Message
44 from .query import Query
45 from .directory import Directory
47 class Database(object):
48 """The :class:`Database` is the highest-level object that notmuch
49 provides. It references a notmuch database, and can be opened in
50 read-only or read-write mode. A :class:`Query` can be derived from
51 or be applied to a specific database to find messages. Also adding
52 and removing messages to the database happens via this
53 object. Modifications to the database are not atmic by default (see
54 :meth:`begin_atomic`) and once a database has been modified, all
55 other database objects pointing to the same data-base will throw an
56 :exc:`XapianError` as the underlying database has been
57 modified. Close and reopen the database to continue working with it.
59 :class:`Database` objects implement the context manager protocol
60 so you can use the :keyword:`with` statement to ensure that the
61 database is properly closed. See :meth:`close` for more
66 Any function in this class can and will throw an
67 :exc:`NotInitializedError` if the database was not intitialized
71 """Class attribute to cache user's default database"""
73 MODE = Enum(['READ_ONLY', 'READ_WRITE'])
74 """Constants: Mode in which to open the database"""
76 """notmuch_database_get_directory"""
77 _get_directory = nmlib.notmuch_database_get_directory
78 _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
79 _get_directory.restype = c_uint
81 """notmuch_database_get_path"""
82 _get_path = nmlib.notmuch_database_get_path
83 _get_path.argtypes = [NotmuchDatabaseP]
84 _get_path.restype = c_char_p
86 """notmuch_database_get_version"""
87 _get_version = nmlib.notmuch_database_get_version
88 _get_version.argtypes = [NotmuchDatabaseP]
89 _get_version.restype = c_uint
91 """notmuch_database_get_revision"""
92 _get_revision = nmlib.notmuch_database_get_revision
93 _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)]
94 _get_revision.restype = c_uint
96 """notmuch_database_open"""
97 _open = nmlib.notmuch_database_open
98 _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
99 _open.restype = c_uint
101 """notmuch_database_upgrade"""
102 _upgrade = nmlib.notmuch_database_upgrade
103 _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
104 _upgrade.restype = c_uint
106 """ notmuch_database_find_message"""
107 _find_message = nmlib.notmuch_database_find_message
108 _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
109 POINTER(NotmuchMessageP)]
110 _find_message.restype = c_uint
112 """notmuch_database_find_message_by_filename"""
113 _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
114 _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
115 POINTER(NotmuchMessageP)]
116 _find_message_by_filename.restype = c_uint
118 """notmuch_database_get_all_tags"""
119 _get_all_tags = nmlib.notmuch_database_get_all_tags
120 _get_all_tags.argtypes = [NotmuchDatabaseP]
121 _get_all_tags.restype = NotmuchTagsP
123 """notmuch_database_create"""
124 _create = nmlib.notmuch_database_create
125 _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
126 _create.restype = c_uint
128 def __init__(self, path = None, create = False,
129 mode = MODE.READ_ONLY):
130 """If *path* is `None`, we will try to read a users notmuch
131 configuration and use his configured database. The location of the
132 configuration file can be specified through the environment variable
133 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
135 If *create* is `True`, the database will always be created in
136 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
138 :param path: Directory to open/create the database in (see
139 above for behavior if `None`)
140 :type path: `str` or `None`
141 :param create: Pass `False` to open an existing, `True` to create a new
144 :param mode: Mode to open a database in. Is always
145 :attr:`MODE`.READ_WRITE when creating a new one.
146 :type mode: :attr:`MODE`
147 :raises: :exc:`NotmuchError` or derived exception in case of
153 # no path specified. use a user's default database
154 if Database._std_db_path is None:
155 #the following line throws a NotmuchError if it fails
156 Database._std_db_path = self._get_user_default_db()
157 path = Database._std_db_path
160 self.open(path, mode)
164 _destroy = nmlib.notmuch_database_destroy
165 _destroy.argtypes = [NotmuchDatabaseP]
166 _destroy.restype = c_uint
170 status = self._destroy(self._db)
171 if status != STATUS.SUCCESS:
172 raise NotmuchError(status)
174 def _assert_db_is_initialized(self):
175 """Raises :exc:`NotInitializedError` if self._db is `None`"""
177 raise NotInitializedError()
179 def create(self, path):
180 """Creates a new notmuch database
182 This function is used by __init__() and usually does not need
183 to be called directly. It wraps the underlying
184 *notmuch_database_create* function and creates a new notmuch
185 database at *path*. It will always return a database in :attr:`MODE`
186 .READ_WRITE mode as creating an empty database for
187 reading only does not make a great deal of sense.
189 :param path: A directory in which we should create the database.
191 :raises: :exc:`NotmuchError` in case of any failure
192 (possibly after printing an error message on stderr).
195 raise NotmuchError(message="Cannot create db, this Database() "
196 "already has an open one.")
198 db = NotmuchDatabaseP()
199 status = Database._create(_str(path), byref(db))
201 if status != STATUS.SUCCESS:
202 raise NotmuchError(status)
206 def open(self, path, mode=0):
207 """Opens an existing database
209 This function is used by __init__() and usually does not need
210 to be called directly. It wraps the underlying
211 *notmuch_database_open* function.
213 :param status: Open the database in read-only or read-write mode
214 :type status: :attr:`MODE`
215 :raises: Raises :exc:`NotmuchError` in case of any failure
216 (possibly after printing an error message on stderr).
218 db = NotmuchDatabaseP()
219 status = Database._open(_str(path), mode, byref(db))
221 if status != STATUS.SUCCESS:
222 raise NotmuchError(status)
226 _close = nmlib.notmuch_database_close
227 _close.argtypes = [NotmuchDatabaseP]
228 _close.restype = c_uint
232 Closes the notmuch database.
236 This function closes the notmuch database. From that point
237 on every method invoked on any object ever derived from
238 the closed database may cease to function and raise a
242 status = self._close(self._db)
243 if status != STATUS.SUCCESS:
244 raise NotmuchError(status)
248 Implements the context manager protocol.
252 def __exit__(self, exc_type, exc_value, traceback):
254 Implements the context manager protocol.
259 """Returns the file path of an open database"""
260 self._assert_db_is_initialized()
261 return Database._get_path(self._db).decode('utf-8')
263 def get_version(self):
264 """Returns the database format version
266 :returns: The database version as positive integer
268 self._assert_db_is_initialized()
269 return Database._get_version(self._db)
271 def get_revision (self):
272 """Returns the committed database revison and UUID
274 :returns: (revison, uuid) The database revision as a positive integer
275 and the UUID of the database.
277 self._assert_db_is_initialized()
279 revision = Database._get_revision(self._db, byref (uuid))
280 return (revision, uuid.value.decode ('utf-8'))
282 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
283 _needs_upgrade.argtypes = [NotmuchDatabaseP]
284 _needs_upgrade.restype = bool
286 def needs_upgrade(self):
287 """Does this database need to be upgraded before writing to it?
289 If this function returns `True` then no functions that modify the
290 database (:meth:`index_file`,
291 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
292 etc.) will work unless :meth:`upgrade` is called successfully first.
294 :returns: `True` or `False`
296 self._assert_db_is_initialized()
297 return self._needs_upgrade(self._db)
300 """Upgrades the current database
302 After opening a database in read-write mode, the client should
303 check if an upgrade is needed (notmuch_database_needs_upgrade) and
304 if so, upgrade with this function before making any modifications.
306 NOT IMPLEMENTED: The optional progress_notify callback can be
307 used by the caller to provide progress indication to the
308 user. If non-NULL it will be called periodically with
309 'progress' as a floating-point value in the range of [0.0..1.0]
310 indicating the progress made so far in the upgrade process.
312 :TODO: catch exceptions, document return values and etc...
314 self._assert_db_is_initialized()
315 status = Database._upgrade(self._db, None, None)
316 # TODO: catch exceptions, document return values and etc
319 _begin_atomic = nmlib.notmuch_database_begin_atomic
320 _begin_atomic.argtypes = [NotmuchDatabaseP]
321 _begin_atomic.restype = c_uint
323 def begin_atomic(self):
324 """Begin an atomic database operation
326 Any modifications performed between a successful
327 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
328 the database atomically. Note that, unlike a typical database
329 transaction, this only ensures atomicity, not durability;
330 neither begin nor end necessarily flush modifications to disk.
332 :returns: :attr:`STATUS`.SUCCESS or raises
333 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
334 Xapian exception occurred; atomic section not entered.
336 *Added in notmuch 0.9*"""
337 self._assert_db_is_initialized()
338 status = self._begin_atomic(self._db)
339 if status != STATUS.SUCCESS:
340 raise NotmuchError(status)
343 _end_atomic = nmlib.notmuch_database_end_atomic
344 _end_atomic.argtypes = [NotmuchDatabaseP]
345 _end_atomic.restype = c_uint
347 def end_atomic(self):
348 """Indicate the end of an atomic database operation
350 See :meth:`begin_atomic` for details.
352 :returns: :attr:`STATUS`.SUCCESS or raises
356 :attr:`STATUS`.XAPIAN_EXCEPTION
357 A Xapian exception occurred; atomic section not
359 :attr:`STATUS`.UNBALANCED_ATOMIC:
360 end_atomic has been called more times than begin_atomic.
362 *Added in notmuch 0.9*"""
363 self._assert_db_is_initialized()
364 status = self._end_atomic(self._db)
365 if status != STATUS.SUCCESS:
366 raise NotmuchError(status)
369 def get_directory(self, path):
370 """Returns a :class:`Directory` of path,
372 :param path: An unicode string containing the path relative to the path
373 of database (see :meth:`get_path`), or else should be an absolute
374 path with initial components that match the path of 'database'.
375 :returns: :class:`Directory` or raises an exception.
376 :raises: :exc:`FileError` if path is not relative database or absolute
377 with initial components same as database.
379 self._assert_db_is_initialized()
381 # sanity checking if path is valid, and make path absolute
382 if path and path[0] == os.sep:
383 # we got an absolute path
384 if not path.startswith(self.get_path()):
385 # but its initial components are not equal to the db path
386 raise FileError('Database().get_directory() called '
387 'with a wrong absolute path')
390 #we got a relative path, make it absolute
391 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
393 dir_p = NotmuchDirectoryP()
394 status = Database._get_directory(self._db, _str(path), byref(dir_p))
396 if status != STATUS.SUCCESS:
397 raise NotmuchError(status)
401 # return the Directory, init it with the absolute path
402 return Directory(abs_dirpath, dir_p, self)
404 _index_file = nmlib.notmuch_database_index_file
405 _index_file.argtypes = [NotmuchDatabaseP, c_char_p,
407 POINTER(NotmuchMessageP)]
408 _index_file.restype = c_uint
410 def index_file(self, filename, sync_maildir_flags=False):
411 """Adds a new message to the database
413 :param filename: should be a path relative to the path of the
414 open database (see :meth:`get_path`), or else should be an
415 absolute filename with initial components that match the
416 path of the database.
418 The file should be a single mail message (not a
419 multi-message mbox) that is expected to remain at its
420 current location, since the notmuch database will reference
421 the filename, and will not copy the entire contents of the
424 :param sync_maildir_flags: If the message contains Maildir
425 flags, we will -depending on the notmuch configuration- sync
426 those tags to initial notmuch tags, if set to `True`. It is
427 `False` by default to remain consistent with the libnotmuch
428 API. You might want to look into the underlying method
429 :meth:`Message.maildir_flags_to_tags`.
431 :returns: On success, we return
433 1) a :class:`Message` object that can be used for things
434 such as adding tags to the just-added message.
435 2) one of the following :attr:`STATUS` values:
437 :attr:`STATUS`.SUCCESS
438 Message successfully added to database.
439 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
440 Message has the same message ID as another message already
441 in the database. The new filename was successfully added
442 to the list of the filenames for the existing message.
444 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
446 :raises: Raises a :exc:`NotmuchError` with the following meaning.
447 If such an exception occurs, nothing was added to the database.
449 :attr:`STATUS`.FILE_ERROR
450 An error occurred trying to open the file, (such as
451 permission denied, or file not found, etc.).
452 :attr:`STATUS`.FILE_NOT_EMAIL
453 The contents of filename don't look like an email
455 :attr:`STATUS`.READ_ONLY_DATABASE
456 Database was opened in read-only mode so no message can
459 self._assert_db_is_initialized()
460 msg_p = NotmuchMessageP()
461 status = self._index_file(self._db, _str(filename), c_void_p(None), byref(msg_p))
463 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
464 raise NotmuchError(status)
466 #construct Message() and return
467 msg = Message(msg_p, self)
468 #automatic sync initial tags from Maildir flags
469 if sync_maildir_flags:
470 msg.maildir_flags_to_tags()
473 def add_message(self, filename, sync_maildir_flags=False):
474 """Deprecated alias for :meth:`index_file`
477 "This function is deprecated and will be removed in the future, use index_file.", DeprecationWarning)
479 return self.index_file(filename, sync_maildir_flags=sync_maildir_flags)
481 _remove_message = nmlib.notmuch_database_remove_message
482 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
483 _remove_message.restype = c_uint
485 def remove_message(self, filename):
486 """Removes a message (filename) from the given notmuch database
488 Note that only this particular filename association is removed from
489 the database. If the same message (as determined by the message ID)
490 is still available via other filenames, then the message will
491 persist in the database for those filenames. When the last filename
492 is removed for a particular message, the database content for that
493 message will be entirely removed.
495 :returns: A :attr:`STATUS` value with the following meaning:
497 :attr:`STATUS`.SUCCESS
498 The last filename was removed and the message was removed
500 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
501 This filename was removed but the message persists in the
502 database with at least one other filename.
504 :raises: Raises a :exc:`NotmuchError` with the following meaning.
505 If such an exception occurs, nothing was removed from the
508 :attr:`STATUS`.READ_ONLY_DATABASE
509 Database was opened in read-only mode so no message can be
512 self._assert_db_is_initialized()
513 status = self._remove_message(self._db, _str(filename))
514 if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
515 raise NotmuchError(status)
518 def find_message(self, msgid):
519 """Returns a :class:`Message` as identified by its message ID
521 Wraps the underlying *notmuch_database_find_message* function.
523 :param msgid: The message ID
524 :type msgid: unicode or str
525 :returns: :class:`Message` or `None` if no message is found.
527 :exc:`OutOfMemoryError`
528 If an Out-of-memory occured while constructing the message.
530 In case of a Xapian Exception. These exceptions
531 include "Database modified" situations, e.g. when the
532 notmuch database has been modified by another program
533 in the meantime. In this case, you should close and
534 reopen the database and retry.
535 :exc:`NotInitializedError` if
536 the database was not intitialized.
538 self._assert_db_is_initialized()
539 msg_p = NotmuchMessageP()
540 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
541 if status != STATUS.SUCCESS:
542 raise NotmuchError(status)
543 return msg_p and Message(msg_p, self) or None
545 def find_message_by_filename(self, filename):
546 """Find a message with the given filename
548 :returns: If the database contains a message with the given
549 filename, then a class:`Message:` is returned. This
550 function returns None if no message is found with the given
553 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
554 constructing the message.
555 :raises: :exc:`XapianError` in case of a Xapian Exception.
556 These exceptions include "Database modified"
557 situations, e.g. when the notmuch database has been
558 modified by another program in the meantime. In this
559 case, you should close and reopen the database and
561 :raises: :exc:`NotInitializedError` if the database was not
564 *Added in notmuch 0.9*"""
565 self._assert_db_is_initialized()
567 msg_p = NotmuchMessageP()
568 status = Database._find_message_by_filename(self._db, _str(filename),
570 if status != STATUS.SUCCESS:
571 raise NotmuchError(status)
572 return msg_p and Message(msg_p, self) or None
574 def get_all_tags(self):
575 """Returns :class:`Tags` with a list of all tags found in the database
577 :returns: :class:`Tags`
578 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
581 self._assert_db_is_initialized()
582 tags_p = Database._get_all_tags(self._db)
584 raise NullPointerError()
585 return Tags(tags_p, self)
587 def create_query(self, querystring):
588 """Returns a :class:`Query` derived from this database
590 This is a shorthand method for doing::
593 # Automatically frees the Database() when 'q' is deleted
595 q = Database(dbpath).create_query('from:"Biene Maja"')
597 # long version, which is functionally equivalent but will keep the
598 # Database in the 'db' variable around after we delete 'q':
600 db = Database(dbpath)
601 q = Query(db,'from:"Biene Maja"')
603 This function is a python extension and not in the underlying C API.
605 return Query(self, querystring)
607 """notmuch_database_status_string"""
608 _status_string = nmlib.notmuch_database_status_string
609 _status_string.argtypes = [NotmuchDatabaseP]
610 _status_string.restype = c_char_p
612 def status_string(self):
613 """Returns the status string of the database
615 This is sometimes used for additional error reporting
617 self._assert_db_is_initialized()
618 s = Database._status_string(self._db)
620 return s.decode('utf-8', 'ignore')
624 return "'Notmuch DB " + self.get_path() + "'"
626 def _get_user_default_db(self):
627 """ Reads a user's notmuch config and returns his db location
629 Throws a NotmuchError if it cannot find it"""
630 config = SafeConfigParser()
631 conf_f = os.getenv('NOTMUCH_CONFIG',
632 os.path.expanduser('~/.notmuch-config'))
633 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
634 if not config.has_option('database', 'path'):
635 raise NotmuchError(message="No DB path specified"
636 " and no user default found")
637 return config.get('database', 'path')
639 """notmuch_database_get_config"""
640 _get_config = nmlib.notmuch_database_get_config
641 _get_config.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(c_char_p)]
642 _get_config.restype = c_uint
644 def get_config(self, key):
645 """Return the value of the given config key.
647 Note that only config values that are stored in the database are
648 searched and returned. The config file is not read.
650 :param key: the config key under which a value should be looked up, it
651 should probably be in the form "section.key"
653 :returns: the config value or the empty string if no value is present
656 :raises: :exc:`NotmuchError` in case of failure.
659 self._assert_db_is_initialized()
660 return_string = c_char_p()
661 status = self._get_config(self._db, _str(key), byref(return_string))
662 if status != STATUS.SUCCESS:
663 raise NotmuchError(status)
664 return return_string.value.decode('utf-8')
666 """notmuch_database_get_config_list"""
667 _get_config_list = nmlib.notmuch_database_get_config_list
668 _get_config_list.argtypes = [NotmuchDatabaseP, c_char_p,
669 POINTER(NotmuchConfigListP)]
670 _get_config_list.restype = c_uint
672 _config_list_valid = nmlib.notmuch_config_list_valid
673 _config_list_valid.argtypes = [NotmuchConfigListP]
674 _config_list_valid.restype = bool
676 _config_list_key = nmlib.notmuch_config_list_key
677 _config_list_key.argtypes = [NotmuchConfigListP]
678 _config_list_key.restype = c_char_p
680 _config_list_value = nmlib.notmuch_config_list_value
681 _config_list_value.argtypes = [NotmuchConfigListP]
682 _config_list_value.restype = c_char_p
684 _config_list_move_to_next = nmlib.notmuch_config_list_move_to_next
685 _config_list_move_to_next.argtypes = [NotmuchConfigListP]
686 _config_list_move_to_next.restype = None
688 _config_list_destroy = nmlib.notmuch_config_list_destroy
689 _config_list_destroy.argtypes = [NotmuchConfigListP]
690 _config_list_destroy.restype = None
692 def get_config_list(self, prefix=''):
693 """Return a generator of key, value pairs where the start of key
694 matches the given prefix
696 Note that only config values that are stored in the database are
697 searched and returned. The config file is not read. If no `prefix` is
698 given all config values are returned.
700 This could be used to get all named queries into a dict for example::
702 queries = {k[6:]: v for k, v in db.get_config_list('query.')}
704 :param prefix: a string by which the keys should be selected
706 :yields: all key-value pairs where `prefix` matches the beginning
709 :raises: :exc:`NotmuchError` in case of failure.
712 self._assert_db_is_initialized()
713 config_list_p = NotmuchConfigListP()
714 status = self._get_config_list(self._db, _str(prefix),
715 byref(config_list_p))
716 if status != STATUS.SUCCESS:
717 raise NotmuchError(status)
718 while self._config_list_valid(config_list_p):
719 key = self._config_list_key(config_list_p).decode('utf-8')
720 value = self._config_list_value(config_list_p).decode('utf-8')
722 self._config_list_move_to_next(config_list_p)
724 def get_configs(self, prefix=''):
725 """Return a dict of key, value pairs where the start of key matches the
728 :param prefix: a string by which the keys should be selected
730 :returns: all key-value pairs where `prefix` matches the beginning
732 :rtype: a dict of str: str
733 :raises: :exc:`NotmuchError` in case of failure.
736 return dict(self.get_config_list(prefix))
738 """notmuch_database_set_config"""
739 _set_config = nmlib.notmuch_database_set_config
740 _set_config.argtypes = [NotmuchDatabaseP, c_char_p, c_char_p]
741 _set_config.restype = c_uint
743 def set_config(self, key, value):
744 """Set a config value in the notmuch database.
746 If an empty string is provided as `value` the `key` is unset!
748 :param key: the key to set
750 :param value: the value to store under `key`
753 :raises: :exc:`NotmuchError` in case of failure.
756 self._assert_db_is_initialized()
757 status = self._set_config(self._db, _str(key), _str(value))
758 if status != STATUS.SUCCESS:
759 raise NotmuchError(status)