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, 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,
350 :param path: An unicode string containing the path relative to the path
351 of database (see :meth:`get_path`), or else should be an absolute
352 path with initial components that match the path of 'database'.
353 :returns: :class:`Directory` or raises an exception.
354 :raises: :exc:`FileError` if path is not relative database or absolute
355 with initial components same as database.
357 self._assert_db_is_initialized()
359 # sanity checking if path is valid, and make path absolute
360 if path and path[0] == os.sep:
361 # we got an absolute path
362 if not path.startswith(self.get_path()):
363 # but its initial components are not equal to the db path
364 raise FileError('Database().get_directory() called '
365 'with a wrong absolute path')
368 #we got a relative path, make it absolute
369 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
371 dir_p = NotmuchDirectoryP()
372 status = Database._get_directory(self._db, _str(path), byref(dir_p))
374 if status != STATUS.SUCCESS:
375 raise NotmuchError(status)
379 # return the Directory, init it with the absolute path
380 return Directory(abs_dirpath, dir_p, self)
382 _add_message = nmlib.notmuch_database_add_message
383 _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
384 POINTER(NotmuchMessageP)]
385 _add_message.restype = c_uint
387 def add_message(self, filename, sync_maildir_flags=False):
388 """Adds a new message to the database
390 :param filename: should be a path relative to the path of the
391 open database (see :meth:`get_path`), or else should be an
392 absolute filename with initial components that match the
393 path of the database.
395 The file should be a single mail message (not a
396 multi-message mbox) that is expected to remain at its
397 current location, since the notmuch database will reference
398 the filename, and will not copy the entire contents of the
401 :param sync_maildir_flags: If the message contains Maildir
402 flags, we will -depending on the notmuch configuration- sync
403 those tags to initial notmuch tags, if set to `True`. It is
404 `False` by default to remain consistent with the libnotmuch
405 API. You might want to look into the underlying method
406 :meth:`Message.maildir_flags_to_tags`.
408 :returns: On success, we return
410 1) a :class:`Message` object that can be used for things
411 such as adding tags to the just-added message.
412 2) one of the following :attr:`STATUS` values:
414 :attr:`STATUS`.SUCCESS
415 Message successfully added to database.
416 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
417 Message has the same message ID as another message already
418 in the database. The new filename was successfully added
419 to the list of the filenames for the existing message.
421 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
423 :raises: Raises a :exc:`NotmuchError` with the following meaning.
424 If such an exception occurs, nothing was added to the database.
426 :attr:`STATUS`.FILE_ERROR
427 An error occurred trying to open the file, (such as
428 permission denied, or file not found, etc.).
429 :attr:`STATUS`.FILE_NOT_EMAIL
430 The contents of filename don't look like an email
432 :attr:`STATUS`.READ_ONLY_DATABASE
433 Database was opened in read-only mode so no message can
436 self._assert_db_is_initialized()
437 msg_p = NotmuchMessageP()
438 status = self._add_message(self._db, _str(filename), byref(msg_p))
440 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
441 raise NotmuchError(status)
443 #construct Message() and return
444 msg = Message(msg_p, self)
445 #automatic sync initial tags from Maildir flags
446 if sync_maildir_flags:
447 msg.maildir_flags_to_tags()
450 _remove_message = nmlib.notmuch_database_remove_message
451 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
452 _remove_message.restype = c_uint
454 def remove_message(self, filename):
455 """Removes a message (filename) from the given notmuch database
457 Note that only this particular filename association is removed from
458 the database. If the same message (as determined by the message ID)
459 is still available via other filenames, then the message will
460 persist in the database for those filenames. When the last filename
461 is removed for a particular message, the database content for that
462 message will be entirely removed.
464 :returns: A :attr:`STATUS` value with the following meaning:
466 :attr:`STATUS`.SUCCESS
467 The last filename was removed and the message was removed
469 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
470 This filename was removed but the message persists in the
471 database with at least one other filename.
473 :raises: Raises a :exc:`NotmuchError` with the following meaning.
474 If such an exception occurs, nothing was removed from the
477 :attr:`STATUS`.READ_ONLY_DATABASE
478 Database was opened in read-only mode so no message can be
481 self._assert_db_is_initialized()
482 return self._remove_message(self._db, _str(filename))
484 def find_message(self, msgid):
485 """Returns a :class:`Message` as identified by its message ID
487 Wraps the underlying *notmuch_database_find_message* function.
489 :param msgid: The message ID
490 :type msgid: unicode or str
491 :returns: :class:`Message` or `None` if no message is found.
493 :exc:`OutOfMemoryError`
494 If an Out-of-memory occured while constructing the message.
496 In case of a Xapian Exception. These exceptions
497 include "Database modified" situations, e.g. when the
498 notmuch database has been modified by another program
499 in the meantime. In this case, you should close and
500 reopen the database and retry.
501 :exc:`NotInitializedError` if
502 the database was not intitialized.
504 self._assert_db_is_initialized()
505 msg_p = NotmuchMessageP()
506 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
507 if status != STATUS.SUCCESS:
508 raise NotmuchError(status)
509 return msg_p and Message(msg_p, self) or None
511 def find_message_by_filename(self, filename):
512 """Find a message with the given filename
514 :returns: If the database contains a message with the given
515 filename, then a class:`Message:` is returned. This
516 function returns None if no message is found with the given
519 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
520 constructing the message.
521 :raises: :exc:`XapianError` in case of a Xapian Exception.
522 These exceptions include "Database modified"
523 situations, e.g. when the notmuch database has been
524 modified by another program in the meantime. In this
525 case, you should close and reopen the database and
527 :raises: :exc:`NotInitializedError` if the database was not
530 *Added in notmuch 0.9*"""
531 self._assert_db_is_initialized()
533 msg_p = NotmuchMessageP()
534 status = Database._find_message_by_filename(self._db, _str(filename),
536 if status != STATUS.SUCCESS:
537 raise NotmuchError(status)
538 return msg_p and Message(msg_p, self) or None
540 def get_all_tags(self):
541 """Returns :class:`Tags` with a list of all tags found in the database
543 :returns: :class:`Tags`
544 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
547 self._assert_db_is_initialized()
548 tags_p = Database._get_all_tags(self._db)
550 raise NullPointerError()
551 return Tags(tags_p, self)
553 def create_query(self, querystring):
554 """Returns a :class:`Query` derived from this database
556 This is a shorthand method for doing::
559 # Automatically frees the Database() when 'q' is deleted
561 q = Database(dbpath).create_query('from:"Biene Maja"')
563 # long version, which is functionally equivalent but will keep the
564 # Database in the 'db' variable around after we delete 'q':
566 db = Database(dbpath)
567 q = Query(db,'from:"Biene Maja"')
569 This function is a python extension and not in the underlying C API.
571 return Query(self, querystring)
574 return "'Notmuch DB " + self.get_path() + "'"
576 def _get_user_default_db(self):
577 """ Reads a user's notmuch config and returns his db location
579 Throws a NotmuchError if it cannot find it"""
582 from configparser import SafeConfigParser
585 from ConfigParser import SafeConfigParser
587 config = SafeConfigParser()
588 conf_f = os.getenv('NOTMUCH_CONFIG',
589 os.path.expanduser('~/.notmuch-config'))
590 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
591 if not config.has_option('database', 'path'):
592 raise NotmuchError(message="No DB path specified"
593 " and no user default found")
594 return config.get('database', 'path')
598 """Property returning a pointer to `notmuch_database_t` or `None`
600 This should normally not be needed by a user (and is not yet
601 guaranteed to remain stable in future versions).