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 .globals import (
38 ReadOnlyDatabaseError,
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_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 # sanity checking if path is valid, and make path absolute
363 if path and path[0] == os.sep:
364 # we got an absolute path
365 if not path.startswith(self.get_path()):
366 # but its initial components are not equal to the db path
367 raise FileError('Database().get_directory() called '
368 'with a wrong absolute path')
371 #we got a relative path, make it absolute
372 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
374 dir_p = NotmuchDirectoryP()
375 status = Database._get_directory(self._db, _str(path), byref(dir_p))
377 if status != STATUS.SUCCESS:
378 raise NotmuchError(status)
382 # return the Directory, init it with the absolute path
383 return Directory(abs_dirpath, dir_p, self)
385 _add_message = nmlib.notmuch_database_add_message
386 _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
387 POINTER(NotmuchMessageP)]
388 _add_message.restype = c_uint
390 def add_message(self, filename, sync_maildir_flags=False):
391 """Adds a new message to the database
393 :param filename: should be a path relative to the path of the
394 open database (see :meth:`get_path`), or else should be an
395 absolute filename with initial components that match the
396 path of the database.
398 The file should be a single mail message (not a
399 multi-message mbox) that is expected to remain at its
400 current location, since the notmuch database will reference
401 the filename, and will not copy the entire contents of the
404 :param sync_maildir_flags: If the message contains Maildir
405 flags, we will -depending on the notmuch configuration- sync
406 those tags to initial notmuch tags, if set to `True`. It is
407 `False` by default to remain consistent with the libnotmuch
408 API. You might want to look into the underlying method
409 :meth:`Message.maildir_flags_to_tags`.
411 :returns: On success, we return
413 1) a :class:`Message` object that can be used for things
414 such as adding tags to the just-added message.
415 2) one of the following :attr:`STATUS` values:
417 :attr:`STATUS`.SUCCESS
418 Message successfully added to database.
419 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
420 Message has the same message ID as another message already
421 in the database. The new filename was successfully added
422 to the list of the filenames for the existing message.
424 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
426 :raises: Raises a :exc:`NotmuchError` with the following meaning.
427 If such an exception occurs, nothing was added to the database.
429 :attr:`STATUS`.FILE_ERROR
430 An error occurred trying to open the file, (such as
431 permission denied, or file not found, etc.).
432 :attr:`STATUS`.FILE_NOT_EMAIL
433 The contents of filename don't look like an email
435 :attr:`STATUS`.READ_ONLY_DATABASE
436 Database was opened in read-only mode so no message can
439 self._assert_db_is_initialized()
440 msg_p = NotmuchMessageP()
441 status = self._add_message(self._db, _str(filename), byref(msg_p))
443 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
444 raise NotmuchError(status)
446 #construct Message() and return
447 msg = Message(msg_p, self)
448 #automatic sync initial tags from Maildir flags
449 if sync_maildir_flags:
450 msg.maildir_flags_to_tags()
453 _remove_message = nmlib.notmuch_database_remove_message
454 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
455 _remove_message.restype = c_uint
457 def remove_message(self, filename):
458 """Removes a message (filename) from the given notmuch database
460 Note that only this particular filename association is removed from
461 the database. If the same message (as determined by the message ID)
462 is still available via other filenames, then the message will
463 persist in the database for those filenames. When the last filename
464 is removed for a particular message, the database content for that
465 message will be entirely removed.
467 :returns: A :attr:`STATUS` value with the following meaning:
469 :attr:`STATUS`.SUCCESS
470 The last filename was removed and the message was removed
472 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
473 This filename was removed but the message persists in the
474 database with at least one other filename.
476 :raises: Raises a :exc:`NotmuchError` with the following meaning.
477 If such an exception occurs, nothing was removed from the
480 :attr:`STATUS`.READ_ONLY_DATABASE
481 Database was opened in read-only mode so no message can be
484 self._assert_db_is_initialized()
485 return self._remove_message(self._db, _str(filename))
487 def find_message(self, msgid):
488 """Returns a :class:`Message` as identified by its message ID
490 Wraps the underlying *notmuch_database_find_message* function.
492 :param msgid: The message ID
493 :type msgid: unicode or str
494 :returns: :class:`Message` or `None` if no message is found.
496 :exc:`OutOfMemoryError`
497 If an Out-of-memory occured while constructing the message.
499 In case of a Xapian Exception. These exceptions
500 include "Database modified" situations, e.g. when the
501 notmuch database has been modified by another program
502 in the meantime. In this case, you should close and
503 reopen the database and retry.
504 :exc:`NotInitializedError` if
505 the database was not intitialized.
507 self._assert_db_is_initialized()
508 msg_p = NotmuchMessageP()
509 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
510 if status != STATUS.SUCCESS:
511 raise NotmuchError(status)
512 return msg_p and Message(msg_p, self) or None
514 def find_message_by_filename(self, filename):
515 """Find a message with the given filename
517 :returns: If the database contains a message with the given
518 filename, then a class:`Message:` is returned. This
519 function returns None if no message is found with the given
522 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
523 constructing the message.
524 :raises: :exc:`XapianError` in case of a Xapian Exception.
525 These exceptions include "Database modified"
526 situations, e.g. when the notmuch database has been
527 modified by another program in the meantime. In this
528 case, you should close and reopen the database and
530 :raises: :exc:`NotInitializedError` if the database was not
532 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
533 opened in read-write mode
535 *Added in notmuch 0.9*"""
536 self._assert_db_is_initialized()
538 # work around libnotmuch calling exit(3), see
539 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
540 # TODO: remove once this issue is resolved
541 if self.mode != Database.MODE.READ_WRITE:
542 raise ReadOnlyDatabaseError('The database has to be opened in '
543 'read-write mode for get_directory')
545 msg_p = NotmuchMessageP()
546 status = Database._find_message_by_filename(self._db, _str(filename),
548 if status != STATUS.SUCCESS:
549 raise NotmuchError(status)
550 return msg_p and Message(msg_p, self) or None
552 def get_all_tags(self):
553 """Returns :class:`Tags` with a list of all tags found in the database
555 :returns: :class:`Tags`
556 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
559 self._assert_db_is_initialized()
560 tags_p = Database._get_all_tags(self._db)
562 raise NullPointerError()
563 return Tags(tags_p, self)
565 def create_query(self, querystring):
566 """Returns a :class:`Query` derived from this database
568 This is a shorthand method for doing::
571 # Automatically frees the Database() when 'q' is deleted
573 q = Database(dbpath).create_query('from:"Biene Maja"')
575 # long version, which is functionally equivalent but will keep the
576 # Database in the 'db' variable around after we delete 'q':
578 db = Database(dbpath)
579 q = Query(db,'from:"Biene Maja"')
581 This function is a python extension and not in the underlying C API.
583 return Query(self, querystring)
586 return "'Notmuch DB " + self.get_path() + "'"
588 def _get_user_default_db(self):
589 """ Reads a user's notmuch config and returns his db location
591 Throws a NotmuchError if it cannot find it"""
594 from configparser import SafeConfigParser
597 from ConfigParser import SafeConfigParser
599 config = SafeConfigParser()
600 conf_f = os.getenv('NOTMUCH_CONFIG',
601 os.path.expanduser('~/.notmuch-config'))
602 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
603 if not config.has_option('database', 'path'):
604 raise NotmuchError(message="No DB path specified"
605 " and no user default found")
606 return config.get('database', 'path')
610 """Property returning a pointer to `notmuch_database_t` or `None`
612 This should normally not be needed by a user (and is not yet
613 guaranteed to remain stable in future versions).