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 (
41 from .message import Message
43 from .query import Query
44 from .directory import Directory
46 class Database(object):
47 """The :class:`Database` is the highest-level object that notmuch
48 provides. It references a notmuch database, and can be opened in
49 read-only or read-write mode. A :class:`Query` can be derived from
50 or be applied to a specific database to find messages. Also adding
51 and removing messages to the database happens via this
52 object. Modifications to the database are not atmic by default (see
53 :meth:`begin_atomic`) and once a database has been modified, all
54 other database objects pointing to the same data-base will throw an
55 :exc:`XapianError` as the underlying database has been
56 modified. Close and reopen the database to continue working with it.
58 :class:`Database` objects implement the context manager protocol
59 so you can use the :keyword:`with` statement to ensure that the
60 database is properly closed. See :meth:`close` for more
65 Any function in this class can and will throw an
66 :exc:`NotInitializedError` if the database was not intitialized
70 """Class attribute to cache user's default database"""
72 MODE = Enum(['READ_ONLY', 'READ_WRITE'])
73 """Constants: Mode in which to open the database"""
75 """notmuch_database_get_directory"""
76 _get_directory = nmlib.notmuch_database_get_directory
77 _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
78 _get_directory.restype = c_uint
80 """notmuch_database_get_path"""
81 _get_path = nmlib.notmuch_database_get_path
82 _get_path.argtypes = [NotmuchDatabaseP]
83 _get_path.restype = c_char_p
85 """notmuch_database_get_version"""
86 _get_version = nmlib.notmuch_database_get_version
87 _get_version.argtypes = [NotmuchDatabaseP]
88 _get_version.restype = c_uint
90 """notmuch_database_get_revision"""
91 _get_revision = nmlib.notmuch_database_get_revision
92 _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)]
93 _get_revision.restype = c_uint
95 """notmuch_database_open"""
96 _open = nmlib.notmuch_database_open
97 _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
98 _open.restype = c_uint
100 """notmuch_database_upgrade"""
101 _upgrade = nmlib.notmuch_database_upgrade
102 _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
103 _upgrade.restype = c_uint
105 """ notmuch_database_find_message"""
106 _find_message = nmlib.notmuch_database_find_message
107 _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
108 POINTER(NotmuchMessageP)]
109 _find_message.restype = c_uint
111 """notmuch_database_find_message_by_filename"""
112 _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
113 _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
114 POINTER(NotmuchMessageP)]
115 _find_message_by_filename.restype = c_uint
117 """notmuch_database_get_all_tags"""
118 _get_all_tags = nmlib.notmuch_database_get_all_tags
119 _get_all_tags.argtypes = [NotmuchDatabaseP]
120 _get_all_tags.restype = NotmuchTagsP
122 """notmuch_database_create"""
123 _create = nmlib.notmuch_database_create
124 _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
125 _create.restype = c_uint
127 def __init__(self, path = None, create = False,
128 mode = MODE.READ_ONLY):
129 """If *path* is `None`, we will try to read a users notmuch
130 configuration and use his configured database. The location of the
131 configuration file can be specified through the environment variable
132 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
134 If *create* is `True`, the database will always be created in
135 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
137 :param path: Directory to open/create the database in (see
138 above for behavior if `None`)
139 :type path: `str` or `None`
140 :param create: Pass `False` to open an existing, `True` to create a new
143 :param mode: Mode to open a database in. Is always
144 :attr:`MODE`.READ_WRITE when creating a new one.
145 :type mode: :attr:`MODE`
146 :raises: :exc:`NotmuchError` or derived exception in case of
152 # no path specified. use a user's default database
153 if Database._std_db_path is None:
154 #the following line throws a NotmuchError if it fails
155 Database._std_db_path = self._get_user_default_db()
156 path = Database._std_db_path
159 self.open(path, mode)
163 _destroy = nmlib.notmuch_database_destroy
164 _destroy.argtypes = [NotmuchDatabaseP]
165 _destroy.restype = c_uint
169 status = self._destroy(self._db)
170 if status != STATUS.SUCCESS:
171 raise NotmuchError(status)
173 def _assert_db_is_initialized(self):
174 """Raises :exc:`NotInitializedError` if self._db is `None`"""
176 raise NotInitializedError()
178 def create(self, path):
179 """Creates a new notmuch database
181 This function is used by __init__() and usually does not need
182 to be called directly. It wraps the underlying
183 *notmuch_database_create* function and creates a new notmuch
184 database at *path*. It will always return a database in :attr:`MODE`
185 .READ_WRITE mode as creating an empty database for
186 reading only does not make a great deal of sense.
188 :param path: A directory in which we should create the database.
190 :raises: :exc:`NotmuchError` in case of any failure
191 (possibly after printing an error message on stderr).
194 raise NotmuchError(message="Cannot create db, this Database() "
195 "already has an open one.")
197 db = NotmuchDatabaseP()
198 status = Database._create(_str(path), byref(db))
200 if status != STATUS.SUCCESS:
201 raise NotmuchError(status)
205 def open(self, path, mode=0):
206 """Opens an existing database
208 This function is used by __init__() and usually does not need
209 to be called directly. It wraps the underlying
210 *notmuch_database_open* function.
212 :param status: Open the database in read-only or read-write mode
213 :type status: :attr:`MODE`
214 :raises: Raises :exc:`NotmuchError` in case of any failure
215 (possibly after printing an error message on stderr).
217 db = NotmuchDatabaseP()
218 status = Database._open(_str(path), mode, byref(db))
220 if status != STATUS.SUCCESS:
221 raise NotmuchError(status)
225 _close = nmlib.notmuch_database_close
226 _close.argtypes = [NotmuchDatabaseP]
227 _close.restype = c_uint
231 Closes the notmuch database.
235 This function closes the notmuch database. From that point
236 on every method invoked on any object ever derived from
237 the closed database may cease to function and raise a
241 status = self._close(self._db)
242 if status != STATUS.SUCCESS:
243 raise NotmuchError(status)
247 Implements the context manager protocol.
251 def __exit__(self, exc_type, exc_value, traceback):
253 Implements the context manager protocol.
258 """Returns the file path of an open database"""
259 self._assert_db_is_initialized()
260 return Database._get_path(self._db).decode('utf-8')
262 def get_version(self):
263 """Returns the database format version
265 :returns: The database version as positive integer
267 self._assert_db_is_initialized()
268 return Database._get_version(self._db)
270 def get_revision (self):
271 """Returns the committed database revison and UUID
273 :returns: (revison, uuid) The database revision as a positive integer
274 and the UUID of the database.
276 self._assert_db_is_initialized()
278 revision = Database._get_revision(self._db, byref (uuid))
279 return (revision, uuid.value.decode ('utf-8'))
281 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
282 _needs_upgrade.argtypes = [NotmuchDatabaseP]
283 _needs_upgrade.restype = bool
285 def needs_upgrade(self):
286 """Does this database need to be upgraded before writing to it?
288 If this function returns `True` then no functions that modify the
289 database (:meth:`index_file`,
290 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
291 etc.) will work unless :meth:`upgrade` is called successfully first.
293 :returns: `True` or `False`
295 self._assert_db_is_initialized()
296 return self._needs_upgrade(self._db)
299 """Upgrades the current database
301 After opening a database in read-write mode, the client should
302 check if an upgrade is needed (notmuch_database_needs_upgrade) and
303 if so, upgrade with this function before making any modifications.
305 NOT IMPLEMENTED: The optional progress_notify callback can be
306 used by the caller to provide progress indication to the
307 user. If non-NULL it will be called periodically with
308 'progress' as a floating-point value in the range of [0.0..1.0]
309 indicating the progress made so far in the upgrade process.
311 :TODO: catch exceptions, document return values and etc...
313 self._assert_db_is_initialized()
314 status = Database._upgrade(self._db, None, None)
315 #TODO: catch exceptions, document return values and etc
318 _begin_atomic = nmlib.notmuch_database_begin_atomic
319 _begin_atomic.argtypes = [NotmuchDatabaseP]
320 _begin_atomic.restype = c_uint
322 def begin_atomic(self):
323 """Begin an atomic database operation
325 Any modifications performed between a successful
326 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
327 the database atomically. Note that, unlike a typical database
328 transaction, this only ensures atomicity, not durability;
329 neither begin nor end necessarily flush modifications to disk.
331 :returns: :attr:`STATUS`.SUCCESS or raises
332 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
333 Xapian exception occurred; atomic section not entered.
335 *Added in notmuch 0.9*"""
336 self._assert_db_is_initialized()
337 status = self._begin_atomic(self._db)
338 if status != STATUS.SUCCESS:
339 raise NotmuchError(status)
342 _end_atomic = nmlib.notmuch_database_end_atomic
343 _end_atomic.argtypes = [NotmuchDatabaseP]
344 _end_atomic.restype = c_uint
346 def end_atomic(self):
347 """Indicate the end of an atomic database operation
349 See :meth:`begin_atomic` for details.
351 :returns: :attr:`STATUS`.SUCCESS or raises
355 :attr:`STATUS`.XAPIAN_EXCEPTION
356 A Xapian exception occurred; atomic section not
358 :attr:`STATUS`.UNBALANCED_ATOMIC:
359 end_atomic has been called more times than begin_atomic.
361 *Added in notmuch 0.9*"""
362 self._assert_db_is_initialized()
363 status = self._end_atomic(self._db)
364 if status != STATUS.SUCCESS:
365 raise NotmuchError(status)
368 def get_directory(self, path):
369 """Returns a :class:`Directory` of path,
371 :param path: An unicode string containing the path relative to the path
372 of database (see :meth:`get_path`), or else should be an absolute
373 path with initial components that match the path of 'database'.
374 :returns: :class:`Directory` or raises an exception.
375 :raises: :exc:`FileError` if path is not relative database or absolute
376 with initial components same as database.
378 self._assert_db_is_initialized()
380 # sanity checking if path is valid, and make path absolute
381 if path and path[0] == os.sep:
382 # we got an absolute path
383 if not path.startswith(self.get_path()):
384 # but its initial components are not equal to the db path
385 raise FileError('Database().get_directory() called '
386 'with a wrong absolute path')
389 #we got a relative path, make it absolute
390 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
392 dir_p = NotmuchDirectoryP()
393 status = Database._get_directory(self._db, _str(path), byref(dir_p))
395 if status != STATUS.SUCCESS:
396 raise NotmuchError(status)
400 # return the Directory, init it with the absolute path
401 return Directory(abs_dirpath, dir_p, self)
403 _index_file = nmlib.notmuch_database_index_file
404 _index_file.argtypes = [NotmuchDatabaseP, c_char_p,
406 POINTER(NotmuchMessageP)]
407 _index_file.restype = c_uint
409 def index_file(self, filename, sync_maildir_flags=False):
410 """Adds a new message to the database
412 :param filename: should be a path relative to the path of the
413 open database (see :meth:`get_path`), or else should be an
414 absolute filename with initial components that match the
415 path of the database.
417 The file should be a single mail message (not a
418 multi-message mbox) that is expected to remain at its
419 current location, since the notmuch database will reference
420 the filename, and will not copy the entire contents of the
423 :param sync_maildir_flags: If the message contains Maildir
424 flags, we will -depending on the notmuch configuration- sync
425 those tags to initial notmuch tags, if set to `True`. It is
426 `False` by default to remain consistent with the libnotmuch
427 API. You might want to look into the underlying method
428 :meth:`Message.maildir_flags_to_tags`.
430 :returns: On success, we return
432 1) a :class:`Message` object that can be used for things
433 such as adding tags to the just-added message.
434 2) one of the following :attr:`STATUS` values:
436 :attr:`STATUS`.SUCCESS
437 Message successfully added to database.
438 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
439 Message has the same message ID as another message already
440 in the database. The new filename was successfully added
441 to the list of the filenames for the existing message.
443 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
445 :raises: Raises a :exc:`NotmuchError` with the following meaning.
446 If such an exception occurs, nothing was added to the database.
448 :attr:`STATUS`.FILE_ERROR
449 An error occurred trying to open the file, (such as
450 permission denied, or file not found, etc.).
451 :attr:`STATUS`.FILE_NOT_EMAIL
452 The contents of filename don't look like an email
454 :attr:`STATUS`.READ_ONLY_DATABASE
455 Database was opened in read-only mode so no message can
458 self._assert_db_is_initialized()
459 msg_p = NotmuchMessageP()
460 status = self._index_file(self._db, _str(filename), c_void_p(None), byref(msg_p))
462 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
463 raise NotmuchError(status)
465 #construct Message() and return
466 msg = Message(msg_p, self)
467 #automatic sync initial tags from Maildir flags
468 if sync_maildir_flags:
469 msg.maildir_flags_to_tags()
472 def add_message(self, filename, sync_maildir_flags=False):
473 """Deprecated alias for :meth:`index_file`
476 "This function is deprecated and will be removed in the future, use index_file.", DeprecationWarning)
478 return self.index_file(filename, sync_maildir_flags=sync_maildir_flags)
480 _remove_message = nmlib.notmuch_database_remove_message
481 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
482 _remove_message.restype = c_uint
484 def remove_message(self, filename):
485 """Removes a message (filename) from the given notmuch database
487 Note that only this particular filename association is removed from
488 the database. If the same message (as determined by the message ID)
489 is still available via other filenames, then the message will
490 persist in the database for those filenames. When the last filename
491 is removed for a particular message, the database content for that
492 message will be entirely removed.
494 :returns: A :attr:`STATUS` value with the following meaning:
496 :attr:`STATUS`.SUCCESS
497 The last filename was removed and the message was removed
499 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
500 This filename was removed but the message persists in the
501 database with at least one other filename.
503 :raises: Raises a :exc:`NotmuchError` with the following meaning.
504 If such an exception occurs, nothing was removed from the
507 :attr:`STATUS`.READ_ONLY_DATABASE
508 Database was opened in read-only mode so no message can be
511 self._assert_db_is_initialized()
512 status = self._remove_message(self._db, _str(filename))
513 if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
514 raise NotmuchError(status)
517 def find_message(self, msgid):
518 """Returns a :class:`Message` as identified by its message ID
520 Wraps the underlying *notmuch_database_find_message* function.
522 :param msgid: The message ID
523 :type msgid: unicode or str
524 :returns: :class:`Message` or `None` if no message is found.
526 :exc:`OutOfMemoryError`
527 If an Out-of-memory occured while constructing the message.
529 In case of a Xapian Exception. These exceptions
530 include "Database modified" situations, e.g. when the
531 notmuch database has been modified by another program
532 in the meantime. In this case, you should close and
533 reopen the database and retry.
534 :exc:`NotInitializedError` if
535 the database was not intitialized.
537 self._assert_db_is_initialized()
538 msg_p = NotmuchMessageP()
539 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
540 if status != STATUS.SUCCESS:
541 raise NotmuchError(status)
542 return msg_p and Message(msg_p, self) or None
544 def find_message_by_filename(self, filename):
545 """Find a message with the given filename
547 :returns: If the database contains a message with the given
548 filename, then a class:`Message:` is returned. This
549 function returns None if no message is found with the given
552 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
553 constructing the message.
554 :raises: :exc:`XapianError` in case of a Xapian Exception.
555 These exceptions include "Database modified"
556 situations, e.g. when the notmuch database has been
557 modified by another program in the meantime. In this
558 case, you should close and reopen the database and
560 :raises: :exc:`NotInitializedError` if the database was not
563 *Added in notmuch 0.9*"""
564 self._assert_db_is_initialized()
566 msg_p = NotmuchMessageP()
567 status = Database._find_message_by_filename(self._db, _str(filename),
569 if status != STATUS.SUCCESS:
570 raise NotmuchError(status)
571 return msg_p and Message(msg_p, self) or None
573 def get_all_tags(self):
574 """Returns :class:`Tags` with a list of all tags found in the database
576 :returns: :class:`Tags`
577 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
580 self._assert_db_is_initialized()
581 tags_p = Database._get_all_tags(self._db)
583 raise NullPointerError()
584 return Tags(tags_p, self)
586 def create_query(self, querystring):
587 """Returns a :class:`Query` derived from this database
589 This is a shorthand method for doing::
592 # Automatically frees the Database() when 'q' is deleted
594 q = Database(dbpath).create_query('from:"Biene Maja"')
596 # long version, which is functionally equivalent but will keep the
597 # Database in the 'db' variable around after we delete 'q':
599 db = Database(dbpath)
600 q = Query(db,'from:"Biene Maja"')
602 This function is a python extension and not in the underlying C API.
604 return Query(self, querystring)
606 """notmuch_database_status_string"""
607 _status_string = nmlib.notmuch_database_status_string
608 _status_string.argtypes = [NotmuchDatabaseP]
609 _status_string.restype = c_char_p
611 def status_string(self):
612 """Returns the status string of the database
614 This is sometimes used for additional error reporting
616 self._assert_db_is_initialized()
617 s = Database._status_string(self._db)
619 return s.decode('utf-8', 'ignore')
623 return "'Notmuch DB " + self.get_path() + "'"
625 def _get_user_default_db(self):
626 """ Reads a user's notmuch config and returns his db location
628 Throws a NotmuchError if it cannot find it"""
629 config = SafeConfigParser()
630 conf_f = os.getenv('NOTMUCH_CONFIG',
631 os.path.expanduser('~/.notmuch-config'))
632 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
633 if not config.has_option('database', 'path'):
634 raise NotmuchError(message="No DB path specified"
635 " and no user default found")
636 return config.get('database', 'path')