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>
22 from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER
23 from .compat import SafeConfigParser
24 from .globals import (
40 from .message import Message
42 from .query import Query
43 from .directory import Directory
45 class Database(object):
46 """The :class:`Database` is the highest-level object that notmuch
47 provides. It references a notmuch database, and can be opened in
48 read-only or read-write mode. A :class:`Query` can be derived from
49 or be applied to a specific database to find messages. Also adding
50 and removing messages to the database happens via this
51 object. Modifications to the database are not atmic by default (see
52 :meth:`begin_atomic`) and once a database has been modified, all
53 other database objects pointing to the same data-base will throw an
54 :exc:`XapianError` as the underlying database has been
55 modified. Close and reopen the database to continue working with it.
57 :class:`Database` objects implement the context manager protocol
58 so you can use the :keyword:`with` statement to ensure that the
59 database is properly closed. See :meth:`close` for more
64 Any function in this class can and will throw an
65 :exc:`NotInitializedError` if the database was not intitialized
69 """Class attribute to cache user's default database"""
71 MODE = Enum(['READ_ONLY', 'READ_WRITE'])
72 """Constants: Mode in which to open the database"""
74 """notmuch_database_get_directory"""
75 _get_directory = nmlib.notmuch_database_get_directory
76 _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
77 _get_directory.restype = c_uint
79 """notmuch_database_get_path"""
80 _get_path = nmlib.notmuch_database_get_path
81 _get_path.argtypes = [NotmuchDatabaseP]
82 _get_path.restype = c_char_p
84 """notmuch_database_get_version"""
85 _get_version = nmlib.notmuch_database_get_version
86 _get_version.argtypes = [NotmuchDatabaseP]
87 _get_version.restype = c_uint
89 """notmuch_database_get_revision"""
90 _get_revision = nmlib.notmuch_database_get_revision
91 _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)]
92 _get_revision.restype = c_uint
94 """notmuch_database_open"""
95 _open = nmlib.notmuch_database_open
96 _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
97 _open.restype = c_uint
99 """notmuch_database_upgrade"""
100 _upgrade = nmlib.notmuch_database_upgrade
101 _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
102 _upgrade.restype = c_uint
104 """ notmuch_database_find_message"""
105 _find_message = nmlib.notmuch_database_find_message
106 _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
107 POINTER(NotmuchMessageP)]
108 _find_message.restype = c_uint
110 """notmuch_database_find_message_by_filename"""
111 _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
112 _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
113 POINTER(NotmuchMessageP)]
114 _find_message_by_filename.restype = c_uint
116 """notmuch_database_get_all_tags"""
117 _get_all_tags = nmlib.notmuch_database_get_all_tags
118 _get_all_tags.argtypes = [NotmuchDatabaseP]
119 _get_all_tags.restype = NotmuchTagsP
121 """notmuch_database_create"""
122 _create = nmlib.notmuch_database_create
123 _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
124 _create.restype = c_uint
126 def __init__(self, path = None, create = False,
127 mode = MODE.READ_ONLY):
128 """If *path* is `None`, we will try to read a users notmuch
129 configuration and use his configured database. The location of the
130 configuration file can be specified through the environment variable
131 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
133 If *create* is `True`, the database will always be created in
134 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
136 :param path: Directory to open/create the database in (see
137 above for behavior if `None`)
138 :type path: `str` or `None`
139 :param create: Pass `False` to open an existing, `True` to create a new
142 :param mode: Mode to open a database in. Is always
143 :attr:`MODE`.READ_WRITE when creating a new one.
144 :type mode: :attr:`MODE`
145 :raises: :exc:`NotmuchError` or derived exception in case of
151 # no path specified. use a user's default database
152 if Database._std_db_path is None:
153 #the following line throws a NotmuchError if it fails
154 Database._std_db_path = self._get_user_default_db()
155 path = Database._std_db_path
158 self.open(path, mode)
162 _destroy = nmlib.notmuch_database_destroy
163 _destroy.argtypes = [NotmuchDatabaseP]
164 _destroy.restype = c_uint
168 status = self._destroy(self._db)
169 if status != STATUS.SUCCESS:
170 raise NotmuchError(status)
172 def _assert_db_is_initialized(self):
173 """Raises :exc:`NotInitializedError` if self._db is `None`"""
175 raise NotInitializedError()
177 def create(self, path):
178 """Creates a new notmuch database
180 This function is used by __init__() and usually does not need
181 to be called directly. It wraps the underlying
182 *notmuch_database_create* function and creates a new notmuch
183 database at *path*. It will always return a database in :attr:`MODE`
184 .READ_WRITE mode as creating an empty database for
185 reading only does not make a great deal of sense.
187 :param path: A directory in which we should create the database.
189 :raises: :exc:`NotmuchError` in case of any failure
190 (possibly after printing an error message on stderr).
193 raise NotmuchError(message="Cannot create db, this Database() "
194 "already has an open one.")
196 db = NotmuchDatabaseP()
197 status = Database._create(_str(path), byref(db))
199 if status != STATUS.SUCCESS:
200 raise NotmuchError(status)
204 def open(self, path, mode=0):
205 """Opens an existing database
207 This function is used by __init__() and usually does not need
208 to be called directly. It wraps the underlying
209 *notmuch_database_open* function.
211 :param status: Open the database in read-only or read-write mode
212 :type status: :attr:`MODE`
213 :raises: Raises :exc:`NotmuchError` in case of any failure
214 (possibly after printing an error message on stderr).
216 db = NotmuchDatabaseP()
217 status = Database._open(_str(path), mode, byref(db))
219 if status != STATUS.SUCCESS:
220 raise NotmuchError(status)
224 _close = nmlib.notmuch_database_close
225 _close.argtypes = [NotmuchDatabaseP]
226 _close.restype = c_uint
230 Closes the notmuch database.
234 This function closes the notmuch database. From that point
235 on every method invoked on any object ever derived from
236 the closed database may cease to function and raise a
240 status = self._close(self._db)
241 if status != STATUS.SUCCESS:
242 raise NotmuchError(status)
246 Implements the context manager protocol.
250 def __exit__(self, exc_type, exc_value, traceback):
252 Implements the context manager protocol.
257 """Returns the file path of an open database"""
258 self._assert_db_is_initialized()
259 return Database._get_path(self._db).decode('utf-8')
261 def get_version(self):
262 """Returns the database format version
264 :returns: The database version as positive integer
266 self._assert_db_is_initialized()
267 return Database._get_version(self._db)
269 def get_revision (self):
270 """Returns the committed database revison and UUID
272 :returns: (revison, uuid) The database revision as a positive integer
273 and the UUID of the database.
275 self._assert_db_is_initialized()
277 revision = Database._get_revision(self._db, byref (uuid))
278 return (revision, uuid.value.decode ('utf-8'))
280 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
281 _needs_upgrade.argtypes = [NotmuchDatabaseP]
282 _needs_upgrade.restype = bool
284 def needs_upgrade(self):
285 """Does this database need to be upgraded before writing to it?
287 If this function returns `True` then no functions that modify the
288 database (:meth:`index_file`,
289 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
290 etc.) will work unless :meth:`upgrade` is called successfully first.
292 :returns: `True` or `False`
294 self._assert_db_is_initialized()
295 return self._needs_upgrade(self._db)
298 """Upgrades the current database
300 After opening a database in read-write mode, the client should
301 check if an upgrade is needed (notmuch_database_needs_upgrade) and
302 if so, upgrade with this function before making any modifications.
304 NOT IMPLEMENTED: The optional progress_notify callback can be
305 used by the caller to provide progress indication to the
306 user. If non-NULL it will be called periodically with
307 'progress' as a floating-point value in the range of [0.0..1.0]
308 indicating the progress made so far in the upgrade process.
310 :TODO: catch exceptions, document return values and etc...
312 self._assert_db_is_initialized()
313 status = Database._upgrade(self._db, None, None)
314 #TODO: catch exceptions, document return values and etc
317 _begin_atomic = nmlib.notmuch_database_begin_atomic
318 _begin_atomic.argtypes = [NotmuchDatabaseP]
319 _begin_atomic.restype = c_uint
321 def begin_atomic(self):
322 """Begin an atomic database operation
324 Any modifications performed between a successful
325 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
326 the database atomically. Note that, unlike a typical database
327 transaction, this only ensures atomicity, not durability;
328 neither begin nor end necessarily flush modifications to disk.
330 :returns: :attr:`STATUS`.SUCCESS or raises
331 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
332 Xapian exception occurred; atomic section not entered.
334 *Added in notmuch 0.9*"""
335 self._assert_db_is_initialized()
336 status = self._begin_atomic(self._db)
337 if status != STATUS.SUCCESS:
338 raise NotmuchError(status)
341 _end_atomic = nmlib.notmuch_database_end_atomic
342 _end_atomic.argtypes = [NotmuchDatabaseP]
343 _end_atomic.restype = c_uint
345 def end_atomic(self):
346 """Indicate the end of an atomic database operation
348 See :meth:`begin_atomic` for details.
350 :returns: :attr:`STATUS`.SUCCESS or raises
354 :attr:`STATUS`.XAPIAN_EXCEPTION
355 A Xapian exception occurred; atomic section not
357 :attr:`STATUS`.UNBALANCED_ATOMIC:
358 end_atomic has been called more times than begin_atomic.
360 *Added in notmuch 0.9*"""
361 self._assert_db_is_initialized()
362 status = self._end_atomic(self._db)
363 if status != STATUS.SUCCESS:
364 raise NotmuchError(status)
367 def get_directory(self, path):
368 """Returns a :class:`Directory` of path,
370 :param path: An unicode string containing the path relative to the path
371 of database (see :meth:`get_path`), or else should be an absolute
372 path with initial components that match the path of 'database'.
373 :returns: :class:`Directory` or raises an exception.
374 :raises: :exc:`FileError` if path is not relative database or absolute
375 with initial components same as database.
377 self._assert_db_is_initialized()
379 # sanity checking if path is valid, and make path absolute
380 if path and path[0] == os.sep:
381 # we got an absolute path
382 if not path.startswith(self.get_path()):
383 # but its initial components are not equal to the db path
384 raise FileError('Database().get_directory() called '
385 'with a wrong absolute path')
388 #we got a relative path, make it absolute
389 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
391 dir_p = NotmuchDirectoryP()
392 status = Database._get_directory(self._db, _str(path), byref(dir_p))
394 if status != STATUS.SUCCESS:
395 raise NotmuchError(status)
399 # return the Directory, init it with the absolute path
400 return Directory(abs_dirpath, dir_p, self)
402 _index_file = nmlib.notmuch_database_index_file
403 _index_file.argtypes = [NotmuchDatabaseP, c_char_p,
405 POINTER(NotmuchMessageP)]
406 _index_file.restype = c_uint
408 def index_file(self, filename, sync_maildir_flags=False):
409 """Adds a new message to the database
411 :param filename: should be a path relative to the path of the
412 open database (see :meth:`get_path`), or else should be an
413 absolute filename with initial components that match the
414 path of the database.
416 The file should be a single mail message (not a
417 multi-message mbox) that is expected to remain at its
418 current location, since the notmuch database will reference
419 the filename, and will not copy the entire contents of the
422 :param sync_maildir_flags: If the message contains Maildir
423 flags, we will -depending on the notmuch configuration- sync
424 those tags to initial notmuch tags, if set to `True`. It is
425 `False` by default to remain consistent with the libnotmuch
426 API. You might want to look into the underlying method
427 :meth:`Message.maildir_flags_to_tags`.
429 :returns: On success, we return
431 1) a :class:`Message` object that can be used for things
432 such as adding tags to the just-added message.
433 2) one of the following :attr:`STATUS` values:
435 :attr:`STATUS`.SUCCESS
436 Message successfully added to database.
437 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
438 Message has the same message ID as another message already
439 in the database. The new filename was successfully added
440 to the list of the filenames for the existing message.
442 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
444 :raises: Raises a :exc:`NotmuchError` with the following meaning.
445 If such an exception occurs, nothing was added to the database.
447 :attr:`STATUS`.FILE_ERROR
448 An error occurred trying to open the file, (such as
449 permission denied, or file not found, etc.).
450 :attr:`STATUS`.FILE_NOT_EMAIL
451 The contents of filename don't look like an email
453 :attr:`STATUS`.READ_ONLY_DATABASE
454 Database was opened in read-only mode so no message can
457 self._assert_db_is_initialized()
458 msg_p = NotmuchMessageP()
459 status = self._index_file(self._db, _str(filename), c_void_p(None), byref(msg_p))
461 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
462 raise NotmuchError(status)
464 #construct Message() and return
465 msg = Message(msg_p, self)
466 #automatic sync initial tags from Maildir flags
467 if sync_maildir_flags:
468 msg.maildir_flags_to_tags()
471 def add_message(self, filename, sync_maildir_flags=False):
472 """Deprecated alias for :meth:`index_file`
474 self.index_file(self, filename, sync_maildir_flags=sync_maildir_flags)
476 _remove_message = nmlib.notmuch_database_remove_message
477 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
478 _remove_message.restype = c_uint
480 def remove_message(self, filename):
481 """Removes a message (filename) from the given notmuch database
483 Note that only this particular filename association is removed from
484 the database. If the same message (as determined by the message ID)
485 is still available via other filenames, then the message will
486 persist in the database for those filenames. When the last filename
487 is removed for a particular message, the database content for that
488 message will be entirely removed.
490 :returns: A :attr:`STATUS` value with the following meaning:
492 :attr:`STATUS`.SUCCESS
493 The last filename was removed and the message was removed
495 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
496 This filename was removed but the message persists in the
497 database with at least one other filename.
499 :raises: Raises a :exc:`NotmuchError` with the following meaning.
500 If such an exception occurs, nothing was removed from the
503 :attr:`STATUS`.READ_ONLY_DATABASE
504 Database was opened in read-only mode so no message can be
507 self._assert_db_is_initialized()
508 status = self._remove_message(self._db, _str(filename))
509 if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
510 raise NotmuchError(status)
513 def find_message(self, msgid):
514 """Returns a :class:`Message` as identified by its message ID
516 Wraps the underlying *notmuch_database_find_message* function.
518 :param msgid: The message ID
519 :type msgid: unicode or str
520 :returns: :class:`Message` or `None` if no message is found.
522 :exc:`OutOfMemoryError`
523 If an Out-of-memory occured while constructing the message.
525 In case of a Xapian Exception. These exceptions
526 include "Database modified" situations, e.g. when the
527 notmuch database has been modified by another program
528 in the meantime. In this case, you should close and
529 reopen the database and retry.
530 :exc:`NotInitializedError` if
531 the database was not intitialized.
533 self._assert_db_is_initialized()
534 msg_p = NotmuchMessageP()
535 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
536 if status != STATUS.SUCCESS:
537 raise NotmuchError(status)
538 return msg_p and Message(msg_p, self) or None
540 def find_message_by_filename(self, filename):
541 """Find a message with the given filename
543 :returns: If the database contains a message with the given
544 filename, then a class:`Message:` is returned. This
545 function returns None if no message is found with the given
548 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
549 constructing the message.
550 :raises: :exc:`XapianError` in case of a Xapian Exception.
551 These exceptions include "Database modified"
552 situations, e.g. when the notmuch database has been
553 modified by another program in the meantime. In this
554 case, you should close and reopen the database and
556 :raises: :exc:`NotInitializedError` if the database was not
559 *Added in notmuch 0.9*"""
560 self._assert_db_is_initialized()
562 msg_p = NotmuchMessageP()
563 status = Database._find_message_by_filename(self._db, _str(filename),
565 if status != STATUS.SUCCESS:
566 raise NotmuchError(status)
567 return msg_p and Message(msg_p, self) or None
569 def get_all_tags(self):
570 """Returns :class:`Tags` with a list of all tags found in the database
572 :returns: :class:`Tags`
573 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
576 self._assert_db_is_initialized()
577 tags_p = Database._get_all_tags(self._db)
579 raise NullPointerError()
580 return Tags(tags_p, self)
582 def create_query(self, querystring):
583 """Returns a :class:`Query` derived from this database
585 This is a shorthand method for doing::
588 # Automatically frees the Database() when 'q' is deleted
590 q = Database(dbpath).create_query('from:"Biene Maja"')
592 # long version, which is functionally equivalent but will keep the
593 # Database in the 'db' variable around after we delete 'q':
595 db = Database(dbpath)
596 q = Query(db,'from:"Biene Maja"')
598 This function is a python extension and not in the underlying C API.
600 return Query(self, querystring)
602 """notmuch_database_status_string"""
603 _status_string = nmlib.notmuch_database_status_string
604 _status_string.argtypes = [NotmuchDatabaseP]
605 _status_string.restype = c_char_p
607 def status_string(self):
608 """Returns the status string of the database
610 This is sometimes used for additional error reporting
612 self._assert_db_is_initialized()
613 s = Database._status_string(self._db)
615 return s.decode('utf-8', 'ignore')
619 return "'Notmuch DB " + self.get_path() + "'"
621 def _get_user_default_db(self):
622 """ Reads a user's notmuch config and returns his db location
624 Throws a NotmuchError if it cannot find it"""
625 config = SafeConfigParser()
626 conf_f = os.getenv('NOTMUCH_CONFIG',
627 os.path.expanduser('~/.notmuch-config'))
628 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
629 if not config.has_option('database', 'path'):
630 raise NotmuchError(message="No DB path specified"
631 " and no user default found")
632 return config.get('database', 'path')