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
70 Do remember that as soon as we tear down (e.g. via `del db`) this
71 object, all underlying derived objects such as queries, threads,
72 messages, tags etc will be freed by the underlying library as well.
73 Accessing these objects will lead to segfaults and other unexpected
74 behavior. See above for more details.
77 """Class attribute to cache user's default database"""
79 MODE = Enum(['READ_ONLY', 'READ_WRITE'])
80 """Constants: Mode in which to open the database"""
82 """notmuch_database_get_directory"""
83 _get_directory = nmlib.notmuch_database_get_directory
84 _get_directory.argtypes = [NotmuchDatabaseP, c_char_p]
85 _get_directory.restype = NotmuchDirectoryP
87 """notmuch_database_get_path"""
88 _get_path = nmlib.notmuch_database_get_path
89 _get_path.argtypes = [NotmuchDatabaseP]
90 _get_path.restype = c_char_p
92 """notmuch_database_get_version"""
93 _get_version = nmlib.notmuch_database_get_version
94 _get_version.argtypes = [NotmuchDatabaseP]
95 _get_version.restype = c_uint
97 """notmuch_database_open"""
98 _open = nmlib.notmuch_database_open
99 _open.argtypes = [c_char_p, c_uint]
100 _open.restype = NotmuchDatabaseP
102 """notmuch_database_upgrade"""
103 _upgrade = nmlib.notmuch_database_upgrade
104 _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
105 _upgrade.restype = c_uint
107 """ notmuch_database_find_message"""
108 _find_message = nmlib.notmuch_database_find_message
109 _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
110 POINTER(NotmuchMessageP)]
111 _find_message.restype = c_uint
113 """notmuch_database_find_message_by_filename"""
114 _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
115 _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
116 POINTER(NotmuchMessageP)]
117 _find_message_by_filename.restype = c_uint
119 """notmuch_database_get_all_tags"""
120 _get_all_tags = nmlib.notmuch_database_get_all_tags
121 _get_all_tags.argtypes = [NotmuchDatabaseP]
122 _get_all_tags.restype = NotmuchTagsP
124 """notmuch_database_create"""
125 _create = nmlib.notmuch_database_create
126 _create.argtypes = [c_char_p]
127 _create.restype = NotmuchDatabaseP
129 def __init__(self, path = None, create = False,
130 mode = MODE.READ_ONLY):
131 """If *path* is `None`, we will try to read a users notmuch
132 configuration and use his configured database. The location of the
133 configuration file can be specified through the environment variable
134 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
136 If *create* is `True`, the database will always be created in
137 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
139 :param path: Directory to open/create the database in (see
140 above for behavior if `None`)
141 :type path: `str` or `None`
142 :param create: Pass `False` to open an existing, `True` to create a new
145 :param mode: Mode to open a database in. Is always
146 :attr:`MODE`.READ_WRITE when creating a new one.
147 :type mode: :attr:`MODE`
148 :raises: :exc:`NotmuchError` or derived exception in case of
154 # no path specified. use a user's default database
155 if Database._std_db_path is None:
156 #the following line throws a NotmuchError if it fails
157 Database._std_db_path = self._get_user_default_db()
158 path = Database._std_db_path
161 self.open(path, mode)
165 _destroy = nmlib.notmuch_database_destroy
166 _destroy.argtypes = [NotmuchDatabaseP]
167 _destroy.restype = None
171 self._destroy(self._db)
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 res = Database._create(_str(path), Database.MODE.READ_WRITE)
201 message="Could not create the specified database")
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 res = Database._open(_str(path), mode)
219 raise NotmuchError(message="Could not open the specified database")
222 _close = nmlib.notmuch_database_close
223 _close.argtypes = [NotmuchDatabaseP]
224 _close.restype = None
228 Closes the notmuch database.
232 This function closes the notmuch database. From that point
233 on every method invoked on any object ever derived from
234 the closed database may cease to function and raise a
238 self._close(self._db)
242 Implements the context manager protocol.
246 def __exit__(self, exc_type, exc_value, traceback):
248 Implements the context manager protocol.
253 """Returns the file path of an open database"""
254 self._assert_db_is_initialized()
255 return Database._get_path(self._db).decode('utf-8')
257 def get_version(self):
258 """Returns the database format version
260 :returns: The database version as positive integer
262 self._assert_db_is_initialized()
263 return Database._get_version(self._db)
265 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
266 _needs_upgrade.argtypes = [NotmuchDatabaseP]
267 _needs_upgrade.restype = bool
269 def needs_upgrade(self):
270 """Does this database need to be upgraded before writing to it?
272 If this function returns `True` then no functions that modify the
273 database (:meth:`add_message`,
274 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
275 etc.) will work unless :meth:`upgrade` is called successfully first.
277 :returns: `True` or `False`
279 self._assert_db_is_initialized()
280 return self._needs_upgrade(self._db)
283 """Upgrades the current database
285 After opening a database in read-write mode, the client should
286 check if an upgrade is needed (notmuch_database_needs_upgrade) and
287 if so, upgrade with this function before making any modifications.
289 NOT IMPLEMENTED: The optional progress_notify callback can be
290 used by the caller to provide progress indication to the
291 user. If non-NULL it will be called periodically with
292 'progress' as a floating-point value in the range of [0.0..1.0]
293 indicating the progress made so far in the upgrade process.
295 :TODO: catch exceptions, document return values and etc...
297 self._assert_db_is_initialized()
298 status = Database._upgrade(self._db, None, None)
299 #TODO: catch exceptions, document return values and etc
302 _begin_atomic = nmlib.notmuch_database_begin_atomic
303 _begin_atomic.argtypes = [NotmuchDatabaseP]
304 _begin_atomic.restype = c_uint
306 def begin_atomic(self):
307 """Begin an atomic database operation
309 Any modifications performed between a successful
310 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
311 the database atomically. Note that, unlike a typical database
312 transaction, this only ensures atomicity, not durability;
313 neither begin nor end necessarily flush modifications to disk.
315 :returns: :attr:`STATUS`.SUCCESS or raises
316 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
317 Xapian exception occurred; atomic section not entered.
319 *Added in notmuch 0.9*"""
320 self._assert_db_is_initialized()
321 status = self._begin_atomic(self._db)
322 if status != STATUS.SUCCESS:
323 raise NotmuchError(status)
326 _end_atomic = nmlib.notmuch_database_end_atomic
327 _end_atomic.argtypes = [NotmuchDatabaseP]
328 _end_atomic.restype = c_uint
330 def end_atomic(self):
331 """Indicate the end of an atomic database operation
333 See :meth:`begin_atomic` for details.
335 :returns: :attr:`STATUS`.SUCCESS or raises
339 :attr:`STATUS`.XAPIAN_EXCEPTION
340 A Xapian exception occurred; atomic section not
342 :attr:`STATUS`.UNBALANCED_ATOMIC:
343 end_atomic has been called more times than begin_atomic.
345 *Added in notmuch 0.9*"""
346 self._assert_db_is_initialized()
347 status = self._end_atomic(self._db)
348 if status != STATUS.SUCCESS:
349 raise NotmuchError(status)
352 def get_directory(self, path):
353 """Returns a :class:`Directory` of path,
354 (creating it if it does not exist(?))
356 :param path: An unicode string containing the path relative to the path
357 of database (see :meth:`get_path`), or else should be an absolute
358 path with initial components that match the path of 'database'.
359 :returns: :class:`Directory` or raises an exception.
360 :raises: :exc:`FileError` if path is not relative database or absolute
361 with initial components same as database.
362 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
363 opened in read-write mode
365 self._assert_db_is_initialized()
367 # work around libnotmuch calling exit(3), see
368 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
369 # TODO: remove once this issue is resolved
370 if self.mode != Database.MODE.READ_WRITE:
371 raise ReadOnlyDatabaseError('The database has to be opened in '
372 'read-write mode for get_directory')
374 # sanity checking if path is valid, and make path absolute
375 if path and path[0] == os.sep:
376 # we got an absolute path
377 if not path.startswith(self.get_path()):
378 # but its initial components are not equal to the db path
379 raise FileError('Database().get_directory() called '
380 'with a wrong absolute path')
383 #we got a relative path, make it absolute
384 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
386 dir_p = Database._get_directory(self._db, _str(path))
388 # return the Directory, init it with the absolute path
389 return Directory(abs_dirpath, dir_p, self)
391 _add_message = nmlib.notmuch_database_add_message
392 _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
393 POINTER(NotmuchMessageP)]
394 _add_message.restype = c_uint
396 def add_message(self, filename, sync_maildir_flags=False):
397 """Adds a new message to the database
399 :param filename: should be a path relative to the path of the
400 open database (see :meth:`get_path`), or else should be an
401 absolute filename with initial components that match the
402 path of the database.
404 The file should be a single mail message (not a
405 multi-message mbox) that is expected to remain at its
406 current location, since the notmuch database will reference
407 the filename, and will not copy the entire contents of the
410 :param sync_maildir_flags: If the message contains Maildir
411 flags, we will -depending on the notmuch configuration- sync
412 those tags to initial notmuch tags, if set to `True`. It is
413 `False` by default to remain consistent with the libnotmuch
414 API. You might want to look into the underlying method
415 :meth:`Message.maildir_flags_to_tags`.
417 :returns: On success, we return
419 1) a :class:`Message` object that can be used for things
420 such as adding tags to the just-added message.
421 2) one of the following :attr:`STATUS` values:
423 :attr:`STATUS`.SUCCESS
424 Message successfully added to database.
425 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
426 Message has the same message ID as another message already
427 in the database. The new filename was successfully added
428 to the list of the filenames for the existing message.
430 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
432 :raises: Raises a :exc:`NotmuchError` with the following meaning.
433 If such an exception occurs, nothing was added to the database.
435 :attr:`STATUS`.FILE_ERROR
436 An error occurred trying to open the file, (such as
437 permission denied, or file not found, etc.).
438 :attr:`STATUS`.FILE_NOT_EMAIL
439 The contents of filename don't look like an email
441 :attr:`STATUS`.READ_ONLY_DATABASE
442 Database was opened in read-only mode so no message can
445 self._assert_db_is_initialized()
446 msg_p = NotmuchMessageP()
447 status = self._add_message(self._db, _str(filename), byref(msg_p))
449 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
450 raise NotmuchError(status)
452 #construct Message() and return
453 msg = Message(msg_p, self)
454 #automatic sync initial tags from Maildir flags
455 if sync_maildir_flags:
456 msg.maildir_flags_to_tags()
459 _remove_message = nmlib.notmuch_database_remove_message
460 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
461 _remove_message.restype = c_uint
463 def remove_message(self, filename):
464 """Removes a message (filename) from the given notmuch database
466 Note that only this particular filename association is removed from
467 the database. If the same message (as determined by the message ID)
468 is still available via other filenames, then the message will
469 persist in the database for those filenames. When the last filename
470 is removed for a particular message, the database content for that
471 message will be entirely removed.
473 :returns: A :attr:`STATUS` value with the following meaning:
475 :attr:`STATUS`.SUCCESS
476 The last filename was removed and the message was removed
478 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
479 This filename was removed but the message persists in the
480 database with at least one other filename.
482 :raises: Raises a :exc:`NotmuchError` with the following meaning.
483 If such an exception occurs, nothing was removed from the
486 :attr:`STATUS`.READ_ONLY_DATABASE
487 Database was opened in read-only mode so no message can be
490 self._assert_db_is_initialized()
491 return self._remove_message(self._db, _str(filename))
493 def find_message(self, msgid):
494 """Returns a :class:`Message` as identified by its message ID
496 Wraps the underlying *notmuch_database_find_message* function.
498 :param msgid: The message ID
499 :type msgid: unicode or str
500 :returns: :class:`Message` or `None` if no message is found.
502 :exc:`OutOfMemoryError`
503 If an Out-of-memory occured while constructing the message.
505 In case of a Xapian Exception. These exceptions
506 include "Database modified" situations, e.g. when the
507 notmuch database has been modified by another program
508 in the meantime. In this case, you should close and
509 reopen the database and retry.
510 :exc:`NotInitializedError` if
511 the database was not intitialized.
513 self._assert_db_is_initialized()
514 msg_p = NotmuchMessageP()
515 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
516 if status != STATUS.SUCCESS:
517 raise NotmuchError(status)
518 return msg_p and Message(msg_p, self) or None
520 def find_message_by_filename(self, filename):
521 """Find a message with the given filename
523 :returns: If the database contains a message with the given
524 filename, then a class:`Message:` is returned. This
525 function returns None if no message is found with the given
528 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
529 constructing the message.
530 :raises: :exc:`XapianError` in case of a Xapian Exception.
531 These exceptions include "Database modified"
532 situations, e.g. when the notmuch database has been
533 modified by another program in the meantime. In this
534 case, you should close and reopen the database and
536 :raises: :exc:`NotInitializedError` if the database was not
538 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
539 opened in read-write mode
541 *Added in notmuch 0.9*"""
542 self._assert_db_is_initialized()
544 # work around libnotmuch calling exit(3), see
545 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
546 # TODO: remove once this issue is resolved
547 if self.mode != Database.MODE.READ_WRITE:
548 raise ReadOnlyDatabaseError('The database has to be opened in '
549 'read-write mode for get_directory')
551 msg_p = NotmuchMessageP()
552 status = Database._find_message_by_filename(self._db, _str(filename),
554 if status != STATUS.SUCCESS:
555 raise NotmuchError(status)
556 return msg_p and Message(msg_p, self) or None
558 def get_all_tags(self):
559 """Returns :class:`Tags` with a list of all tags found in the database
561 :returns: :class:`Tags`
562 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
565 self._assert_db_is_initialized()
566 tags_p = Database._get_all_tags(self._db)
568 raise NullPointerError()
569 return Tags(tags_p, self)
571 def create_query(self, querystring):
572 """Returns a :class:`Query` derived from this database
574 This is a shorthand method for doing::
577 # Automatically frees the Database() when 'q' is deleted
579 q = Database(dbpath).create_query('from:"Biene Maja"')
581 # long version, which is functionally equivalent but will keep the
582 # Database in the 'db' variable around after we delete 'q':
584 db = Database(dbpath)
585 q = Query(db,'from:"Biene Maja"')
587 This function is a python extension and not in the underlying C API.
589 return Query(self, querystring)
592 return "'Notmuch DB " + self.get_path() + "'"
594 def _get_user_default_db(self):
595 """ Reads a user's notmuch config and returns his db location
597 Throws a NotmuchError if it cannot find it"""
600 from configparser import SafeConfigParser
603 from ConfigParser import SafeConfigParser
605 config = SafeConfigParser()
606 conf_f = os.getenv('NOTMUCH_CONFIG',
607 os.path.expanduser('~/.notmuch-config'))
608 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
609 if not config.has_option('database', 'path'):
610 raise NotmuchError(message="No DB path specified"
611 " and no user default found")
612 return config.get('database', 'path')
616 """Property returning a pointer to `notmuch_database_t` or `None`
618 This should normally not be needed by a user (and is not yet
619 guaranteed to remain stable in future versions).