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 <http://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 notmuch.globals import (
38 ReadOnlyDatabaseError,
40 from notmuch.message import Message
41 from notmuch.tag import Tags
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]
77 _get_directory.restype = NotmuchDirectoryP
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_open"""
90 _open = nmlib.notmuch_database_open
91 _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
92 _open.restype = c_uint
94 """notmuch_database_upgrade"""
95 _upgrade = nmlib.notmuch_database_upgrade
96 _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
97 _upgrade.restype = c_uint
99 """ notmuch_database_find_message"""
100 _find_message = nmlib.notmuch_database_find_message
101 _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
102 POINTER(NotmuchMessageP)]
103 _find_message.restype = c_uint
105 """notmuch_database_find_message_by_filename"""
106 _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
107 _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
108 POINTER(NotmuchMessageP)]
109 _find_message_by_filename.restype = c_uint
111 """notmuch_database_get_all_tags"""
112 _get_all_tags = nmlib.notmuch_database_get_all_tags
113 _get_all_tags.argtypes = [NotmuchDatabaseP]
114 _get_all_tags.restype = NotmuchTagsP
116 """notmuch_database_create"""
117 _create = nmlib.notmuch_database_create
118 _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
119 _create.restype = c_uint
121 def __init__(self, path = None, create = False,
122 mode = MODE.READ_ONLY):
123 """If *path* is `None`, we will try to read a users notmuch
124 configuration and use his configured database. The location of the
125 configuration file can be specified through the environment variable
126 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
128 If *create* is `True`, the database will always be created in
129 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
131 :param path: Directory to open/create the database in (see
132 above for behavior if `None`)
133 :type path: `str` or `None`
134 :param create: Pass `False` to open an existing, `True` to create a new
137 :param mode: Mode to open a database in. Is always
138 :attr:`MODE`.READ_WRITE when creating a new one.
139 :type mode: :attr:`MODE`
140 :raises: :exc:`NotmuchError` or derived exception in case of
146 # no path specified. use a user's default database
147 if Database._std_db_path is None:
148 #the following line throws a NotmuchError if it fails
149 Database._std_db_path = self._get_user_default_db()
150 path = Database._std_db_path
153 self.open(path, mode)
157 _destroy = nmlib.notmuch_database_destroy
158 _destroy.argtypes = [NotmuchDatabaseP]
159 _destroy.restype = None
163 self._destroy(self._db)
165 def _assert_db_is_initialized(self):
166 """Raises :exc:`NotInitializedError` if self._db is `None`"""
168 raise NotInitializedError()
170 def create(self, path):
171 """Creates a new notmuch database
173 This function is used by __init__() and usually does not need
174 to be called directly. It wraps the underlying
175 *notmuch_database_create* function and creates a new notmuch
176 database at *path*. It will always return a database in :attr:`MODE`
177 .READ_WRITE mode as creating an empty database for
178 reading only does not make a great deal of sense.
180 :param path: A directory in which we should create the database.
182 :raises: :exc:`NotmuchError` in case of any failure
183 (possibly after printing an error message on stderr).
186 raise NotmuchError(message="Cannot create db, this Database() "
187 "already has an open one.")
189 db = NotmuchDatabaseP()
190 status = Database._create(_str(path), Database.MODE.READ_WRITE, byref(db))
192 if status != STATUS.SUCCESS:
193 raise NotmuchError(status)
197 def open(self, path, mode=0):
198 """Opens an existing database
200 This function is used by __init__() and usually does not need
201 to be called directly. It wraps the underlying
202 *notmuch_database_open* function.
204 :param status: Open the database in read-only or read-write mode
205 :type status: :attr:`MODE`
206 :raises: Raises :exc:`NotmuchError` in case of any failure
207 (possibly after printing an error message on stderr).
209 db = NotmuchDatabaseP()
210 status = Database._open(_str(path), mode, byref(db))
212 if status != STATUS.SUCCESS:
213 raise NotmuchError(status)
217 _close = nmlib.notmuch_database_close
218 _close.argtypes = [NotmuchDatabaseP]
219 _close.restype = None
223 Closes the notmuch database.
227 This function closes the notmuch database. From that point
228 on every method invoked on any object ever derived from
229 the closed database may cease to function and raise a
233 self._close(self._db)
237 Implements the context manager protocol.
241 def __exit__(self, exc_type, exc_value, traceback):
243 Implements the context manager protocol.
248 """Returns the file path of an open database"""
249 self._assert_db_is_initialized()
250 return Database._get_path(self._db).decode('utf-8')
252 def get_version(self):
253 """Returns the database format version
255 :returns: The database version as positive integer
257 self._assert_db_is_initialized()
258 return Database._get_version(self._db)
260 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
261 _needs_upgrade.argtypes = [NotmuchDatabaseP]
262 _needs_upgrade.restype = bool
264 def needs_upgrade(self):
265 """Does this database need to be upgraded before writing to it?
267 If this function returns `True` then no functions that modify the
268 database (:meth:`add_message`,
269 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
270 etc.) will work unless :meth:`upgrade` is called successfully first.
272 :returns: `True` or `False`
274 self._assert_db_is_initialized()
275 return self._needs_upgrade(self._db)
278 """Upgrades the current database
280 After opening a database in read-write mode, the client should
281 check if an upgrade is needed (notmuch_database_needs_upgrade) and
282 if so, upgrade with this function before making any modifications.
284 NOT IMPLEMENTED: The optional progress_notify callback can be
285 used by the caller to provide progress indication to the
286 user. If non-NULL it will be called periodically with
287 'progress' as a floating-point value in the range of [0.0..1.0]
288 indicating the progress made so far in the upgrade process.
290 :TODO: catch exceptions, document return values and etc...
292 self._assert_db_is_initialized()
293 status = Database._upgrade(self._db, None, None)
294 #TODO: catch exceptions, document return values and etc
297 _begin_atomic = nmlib.notmuch_database_begin_atomic
298 _begin_atomic.argtypes = [NotmuchDatabaseP]
299 _begin_atomic.restype = c_uint
301 def begin_atomic(self):
302 """Begin an atomic database operation
304 Any modifications performed between a successful
305 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
306 the database atomically. Note that, unlike a typical database
307 transaction, this only ensures atomicity, not durability;
308 neither begin nor end necessarily flush modifications to disk.
310 :returns: :attr:`STATUS`.SUCCESS or raises
311 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
312 Xapian exception occurred; atomic section not entered.
314 *Added in notmuch 0.9*"""
315 self._assert_db_is_initialized()
316 status = self._begin_atomic(self._db)
317 if status != STATUS.SUCCESS:
318 raise NotmuchError(status)
321 _end_atomic = nmlib.notmuch_database_end_atomic
322 _end_atomic.argtypes = [NotmuchDatabaseP]
323 _end_atomic.restype = c_uint
325 def end_atomic(self):
326 """Indicate the end of an atomic database operation
328 See :meth:`begin_atomic` for details.
330 :returns: :attr:`STATUS`.SUCCESS or raises
334 :attr:`STATUS`.XAPIAN_EXCEPTION
335 A Xapian exception occurred; atomic section not
337 :attr:`STATUS`.UNBALANCED_ATOMIC:
338 end_atomic has been called more times than begin_atomic.
340 *Added in notmuch 0.9*"""
341 self._assert_db_is_initialized()
342 status = self._end_atomic(self._db)
343 if status != STATUS.SUCCESS:
344 raise NotmuchError(status)
347 def get_directory(self, path):
348 """Returns a :class:`Directory` of path,
349 (creating it if it does not exist(?))
351 :param path: An unicode string containing the path relative to the path
352 of database (see :meth:`get_path`), or else should be an absolute
353 path with initial components that match the path of 'database'.
354 :returns: :class:`Directory` or raises an exception.
355 :raises: :exc:`FileError` if path is not relative database or absolute
356 with initial components same as database.
357 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
358 opened in read-write mode
360 self._assert_db_is_initialized()
362 # work around libnotmuch calling exit(3), see
363 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
364 # TODO: remove once this issue is resolved
365 if self.mode != Database.MODE.READ_WRITE:
366 raise ReadOnlyDatabaseError('The database has to be opened in '
367 'read-write mode for get_directory')
369 # sanity checking if path is valid, and make path absolute
370 if path and path[0] == os.sep:
371 # we got an absolute path
372 if not path.startswith(self.get_path()):
373 # but its initial components are not equal to the db path
374 raise FileError('Database().get_directory() called '
375 'with a wrong absolute path')
378 #we got a relative path, make it absolute
379 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
381 dir_p = Database._get_directory(self._db, _str(path))
383 # return the Directory, init it with the absolute path
384 return Directory(abs_dirpath, dir_p, self)
386 _add_message = nmlib.notmuch_database_add_message
387 _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
388 POINTER(NotmuchMessageP)]
389 _add_message.restype = c_uint
391 def add_message(self, filename, sync_maildir_flags=False):
392 """Adds a new message to the database
394 :param filename: should be a path relative to the path of the
395 open database (see :meth:`get_path`), or else should be an
396 absolute filename with initial components that match the
397 path of the database.
399 The file should be a single mail message (not a
400 multi-message mbox) that is expected to remain at its
401 current location, since the notmuch database will reference
402 the filename, and will not copy the entire contents of the
405 :param sync_maildir_flags: If the message contains Maildir
406 flags, we will -depending on the notmuch configuration- sync
407 those tags to initial notmuch tags, if set to `True`. It is
408 `False` by default to remain consistent with the libnotmuch
409 API. You might want to look into the underlying method
410 :meth:`Message.maildir_flags_to_tags`.
412 :returns: On success, we return
414 1) a :class:`Message` object that can be used for things
415 such as adding tags to the just-added message.
416 2) one of the following :attr:`STATUS` values:
418 :attr:`STATUS`.SUCCESS
419 Message successfully added to database.
420 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
421 Message has the same message ID as another message already
422 in the database. The new filename was successfully added
423 to the list of the filenames for the existing message.
425 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
427 :raises: Raises a :exc:`NotmuchError` with the following meaning.
428 If such an exception occurs, nothing was added to the database.
430 :attr:`STATUS`.FILE_ERROR
431 An error occurred trying to open the file, (such as
432 permission denied, or file not found, etc.).
433 :attr:`STATUS`.FILE_NOT_EMAIL
434 The contents of filename don't look like an email
436 :attr:`STATUS`.READ_ONLY_DATABASE
437 Database was opened in read-only mode so no message can
440 self._assert_db_is_initialized()
441 msg_p = NotmuchMessageP()
442 status = self._add_message(self._db, _str(filename), byref(msg_p))
444 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
445 raise NotmuchError(status)
447 #construct Message() and return
448 msg = Message(msg_p, self)
449 #automatic sync initial tags from Maildir flags
450 if sync_maildir_flags:
451 msg.maildir_flags_to_tags()
454 _remove_message = nmlib.notmuch_database_remove_message
455 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
456 _remove_message.restype = c_uint
458 def remove_message(self, filename):
459 """Removes a message (filename) from the given notmuch database
461 Note that only this particular filename association is removed from
462 the database. If the same message (as determined by the message ID)
463 is still available via other filenames, then the message will
464 persist in the database for those filenames. When the last filename
465 is removed for a particular message, the database content for that
466 message will be entirely removed.
468 :returns: A :attr:`STATUS` value with the following meaning:
470 :attr:`STATUS`.SUCCESS
471 The last filename was removed and the message was removed
473 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
474 This filename was removed but the message persists in the
475 database with at least one other filename.
477 :raises: Raises a :exc:`NotmuchError` with the following meaning.
478 If such an exception occurs, nothing was removed from the
481 :attr:`STATUS`.READ_ONLY_DATABASE
482 Database was opened in read-only mode so no message can be
485 self._assert_db_is_initialized()
486 return self._remove_message(self._db, _str(filename))
488 def find_message(self, msgid):
489 """Returns a :class:`Message` as identified by its message ID
491 Wraps the underlying *notmuch_database_find_message* function.
493 :param msgid: The message ID
494 :type msgid: unicode or str
495 :returns: :class:`Message` or `None` if no message is found.
497 :exc:`OutOfMemoryError`
498 If an Out-of-memory occured while constructing the message.
500 In case of a Xapian Exception. These exceptions
501 include "Database modified" situations, e.g. when the
502 notmuch database has been modified by another program
503 in the meantime. In this case, you should close and
504 reopen the database and retry.
505 :exc:`NotInitializedError` if
506 the database was not intitialized.
508 self._assert_db_is_initialized()
509 msg_p = NotmuchMessageP()
510 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
511 if status != STATUS.SUCCESS:
512 raise NotmuchError(status)
513 return msg_p and Message(msg_p, self) or None
515 def find_message_by_filename(self, filename):
516 """Find a message with the given filename
518 :returns: If the database contains a message with the given
519 filename, then a class:`Message:` is returned. This
520 function returns None if no message is found with the given
523 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
524 constructing the message.
525 :raises: :exc:`XapianError` in case of a Xapian Exception.
526 These exceptions include "Database modified"
527 situations, e.g. when the notmuch database has been
528 modified by another program in the meantime. In this
529 case, you should close and reopen the database and
531 :raises: :exc:`NotInitializedError` if the database was not
533 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
534 opened in read-write mode
536 *Added in notmuch 0.9*"""
537 self._assert_db_is_initialized()
539 # work around libnotmuch calling exit(3), see
540 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
541 # TODO: remove once this issue is resolved
542 if self.mode != Database.MODE.READ_WRITE:
543 raise ReadOnlyDatabaseError('The database has to be opened in '
544 'read-write mode for get_directory')
546 msg_p = NotmuchMessageP()
547 status = Database._find_message_by_filename(self._db, _str(filename),
549 if status != STATUS.SUCCESS:
550 raise NotmuchError(status)
551 return msg_p and Message(msg_p, self) or None
553 def get_all_tags(self):
554 """Returns :class:`Tags` with a list of all tags found in the database
556 :returns: :class:`Tags`
557 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
560 self._assert_db_is_initialized()
561 tags_p = Database._get_all_tags(self._db)
563 raise NullPointerError()
564 return Tags(tags_p, self)
566 def create_query(self, querystring):
567 """Returns a :class:`Query` derived from this database
569 This is a shorthand method for doing::
572 # Automatically frees the Database() when 'q' is deleted
574 q = Database(dbpath).create_query('from:"Biene Maja"')
576 # long version, which is functionally equivalent but will keep the
577 # Database in the 'db' variable around after we delete 'q':
579 db = Database(dbpath)
580 q = Query(db,'from:"Biene Maja"')
582 This function is a python extension and not in the underlying C API.
584 return Query(self, querystring)
587 return "'Notmuch DB " + self.get_path() + "'"
589 def _get_user_default_db(self):
590 """ Reads a user's notmuch config and returns his db location
592 Throws a NotmuchError if it cannot find it"""
595 from configparser import SafeConfigParser
598 from ConfigParser import SafeConfigParser
600 config = SafeConfigParser()
601 conf_f = os.getenv('NOTMUCH_CONFIG',
602 os.path.expanduser('~/.notmuch-config'))
603 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
604 if not config.has_option('database', 'path'):
605 raise NotmuchError(message="No DB path specified"
606 " and no user default found")
607 return config.get('database', 'path')
611 """Property returning a pointer to `notmuch_database_t` or `None`
613 This should normally not be needed by a user (and is not yet
614 guaranteed to remain stable in future versions).