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_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 = c_uint
163 status = self._destroy(self._db)
164 if status != STATUS.SUCCESS:
165 raise NotmuchError(status)
167 def _assert_db_is_initialized(self):
168 """Raises :exc:`NotInitializedError` if self._db is `None`"""
170 raise NotInitializedError()
172 def create(self, path):
173 """Creates a new notmuch database
175 This function is used by __init__() and usually does not need
176 to be called directly. It wraps the underlying
177 *notmuch_database_create* function and creates a new notmuch
178 database at *path*. It will always return a database in :attr:`MODE`
179 .READ_WRITE mode as creating an empty database for
180 reading only does not make a great deal of sense.
182 :param path: A directory in which we should create the database.
184 :raises: :exc:`NotmuchError` in case of any failure
185 (possibly after printing an error message on stderr).
188 raise NotmuchError(message="Cannot create db, this Database() "
189 "already has an open one.")
191 db = NotmuchDatabaseP()
192 status = Database._create(_str(path), byref(db))
194 if status != STATUS.SUCCESS:
195 raise NotmuchError(status)
199 def open(self, path, mode=0):
200 """Opens an existing database
202 This function is used by __init__() and usually does not need
203 to be called directly. It wraps the underlying
204 *notmuch_database_open* function.
206 :param status: Open the database in read-only or read-write mode
207 :type status: :attr:`MODE`
208 :raises: Raises :exc:`NotmuchError` in case of any failure
209 (possibly after printing an error message on stderr).
211 db = NotmuchDatabaseP()
212 status = Database._open(_str(path), mode, byref(db))
214 if status != STATUS.SUCCESS:
215 raise NotmuchError(status)
219 _close = nmlib.notmuch_database_close
220 _close.argtypes = [NotmuchDatabaseP]
221 _close.restype = c_uint
225 Closes the notmuch database.
229 This function closes the notmuch database. From that point
230 on every method invoked on any object ever derived from
231 the closed database may cease to function and raise a
235 status = self._close(self._db)
236 if status != STATUS.SUCCESS:
237 raise NotmuchError(status)
241 Implements the context manager protocol.
245 def __exit__(self, exc_type, exc_value, traceback):
247 Implements the context manager protocol.
252 """Returns the file path of an open database"""
253 self._assert_db_is_initialized()
254 return Database._get_path(self._db).decode('utf-8')
256 def get_version(self):
257 """Returns the database format version
259 :returns: The database version as positive integer
261 self._assert_db_is_initialized()
262 return Database._get_version(self._db)
264 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
265 _needs_upgrade.argtypes = [NotmuchDatabaseP]
266 _needs_upgrade.restype = bool
268 def needs_upgrade(self):
269 """Does this database need to be upgraded before writing to it?
271 If this function returns `True` then no functions that modify the
272 database (:meth:`add_message`,
273 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
274 etc.) will work unless :meth:`upgrade` is called successfully first.
276 :returns: `True` or `False`
278 self._assert_db_is_initialized()
279 return self._needs_upgrade(self._db)
282 """Upgrades the current database
284 After opening a database in read-write mode, the client should
285 check if an upgrade is needed (notmuch_database_needs_upgrade) and
286 if so, upgrade with this function before making any modifications.
288 NOT IMPLEMENTED: The optional progress_notify callback can be
289 used by the caller to provide progress indication to the
290 user. If non-NULL it will be called periodically with
291 'progress' as a floating-point value in the range of [0.0..1.0]
292 indicating the progress made so far in the upgrade process.
294 :TODO: catch exceptions, document return values and etc...
296 self._assert_db_is_initialized()
297 status = Database._upgrade(self._db, None, None)
298 #TODO: catch exceptions, document return values and etc
301 _begin_atomic = nmlib.notmuch_database_begin_atomic
302 _begin_atomic.argtypes = [NotmuchDatabaseP]
303 _begin_atomic.restype = c_uint
305 def begin_atomic(self):
306 """Begin an atomic database operation
308 Any modifications performed between a successful
309 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
310 the database atomically. Note that, unlike a typical database
311 transaction, this only ensures atomicity, not durability;
312 neither begin nor end necessarily flush modifications to disk.
314 :returns: :attr:`STATUS`.SUCCESS or raises
315 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
316 Xapian exception occurred; atomic section not entered.
318 *Added in notmuch 0.9*"""
319 self._assert_db_is_initialized()
320 status = self._begin_atomic(self._db)
321 if status != STATUS.SUCCESS:
322 raise NotmuchError(status)
325 _end_atomic = nmlib.notmuch_database_end_atomic
326 _end_atomic.argtypes = [NotmuchDatabaseP]
327 _end_atomic.restype = c_uint
329 def end_atomic(self):
330 """Indicate the end of an atomic database operation
332 See :meth:`begin_atomic` for details.
334 :returns: :attr:`STATUS`.SUCCESS or raises
338 :attr:`STATUS`.XAPIAN_EXCEPTION
339 A Xapian exception occurred; atomic section not
341 :attr:`STATUS`.UNBALANCED_ATOMIC:
342 end_atomic has been called more times than begin_atomic.
344 *Added in notmuch 0.9*"""
345 self._assert_db_is_initialized()
346 status = self._end_atomic(self._db)
347 if status != STATUS.SUCCESS:
348 raise NotmuchError(status)
351 def get_directory(self, path):
352 """Returns a :class:`Directory` of path,
354 :param path: An unicode string containing the path relative to the path
355 of database (see :meth:`get_path`), or else should be an absolute
356 path with initial components that match the path of 'database'.
357 :returns: :class:`Directory` or raises an exception.
358 :raises: :exc:`FileError` if path is not relative database or absolute
359 with initial components same as database.
361 self._assert_db_is_initialized()
363 # sanity checking if path is valid, and make path absolute
364 if path and path[0] == os.sep:
365 # we got an absolute path
366 if not path.startswith(self.get_path()):
367 # but its initial components are not equal to the db path
368 raise FileError('Database().get_directory() called '
369 'with a wrong absolute path')
372 #we got a relative path, make it absolute
373 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
375 dir_p = NotmuchDirectoryP()
376 status = Database._get_directory(self._db, _str(path), byref(dir_p))
378 if status != STATUS.SUCCESS:
379 raise NotmuchError(status)
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 status = self._remove_message(self._db, _str(filename))
487 if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
488 raise NotmuchError(status)
491 def find_message(self, msgid):
492 """Returns a :class:`Message` as identified by its message ID
494 Wraps the underlying *notmuch_database_find_message* function.
496 :param msgid: The message ID
497 :type msgid: unicode or str
498 :returns: :class:`Message` or `None` if no message is found.
500 :exc:`OutOfMemoryError`
501 If an Out-of-memory occured while constructing the message.
503 In case of a Xapian Exception. These exceptions
504 include "Database modified" situations, e.g. when the
505 notmuch database has been modified by another program
506 in the meantime. In this case, you should close and
507 reopen the database and retry.
508 :exc:`NotInitializedError` if
509 the database was not intitialized.
511 self._assert_db_is_initialized()
512 msg_p = NotmuchMessageP()
513 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
514 if status != STATUS.SUCCESS:
515 raise NotmuchError(status)
516 return msg_p and Message(msg_p, self) or None
518 def find_message_by_filename(self, filename):
519 """Find a message with the given filename
521 :returns: If the database contains a message with the given
522 filename, then a class:`Message:` is returned. This
523 function returns None if no message is found with the given
526 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
527 constructing the message.
528 :raises: :exc:`XapianError` in case of a Xapian Exception.
529 These exceptions include "Database modified"
530 situations, e.g. when the notmuch database has been
531 modified by another program in the meantime. In this
532 case, you should close and reopen the database and
534 :raises: :exc:`NotInitializedError` if the database was not
537 *Added in notmuch 0.9*"""
538 self._assert_db_is_initialized()
540 msg_p = NotmuchMessageP()
541 status = Database._find_message_by_filename(self._db, _str(filename),
543 if status != STATUS.SUCCESS:
544 raise NotmuchError(status)
545 return msg_p and Message(msg_p, self) or None
547 def get_all_tags(self):
548 """Returns :class:`Tags` with a list of all tags found in the database
550 :returns: :class:`Tags`
551 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
554 self._assert_db_is_initialized()
555 tags_p = Database._get_all_tags(self._db)
557 raise NullPointerError()
558 return Tags(tags_p, self)
560 def create_query(self, querystring):
561 """Returns a :class:`Query` derived from this database
563 This is a shorthand method for doing::
566 # Automatically frees the Database() when 'q' is deleted
568 q = Database(dbpath).create_query('from:"Biene Maja"')
570 # long version, which is functionally equivalent but will keep the
571 # Database in the 'db' variable around after we delete 'q':
573 db = Database(dbpath)
574 q = Query(db,'from:"Biene Maja"')
576 This function is a python extension and not in the underlying C API.
578 return Query(self, querystring)
580 """notmuch_database_status_string"""
581 _status_string = nmlib.notmuch_database_status_string
582 _status_string.argtypes = [NotmuchDatabaseP]
583 _status_string.restype = c_char_p
585 def status_string(self):
586 """Returns the status string of the database
588 This is sometimes used for additional error reporting
590 self._assert_db_is_initialized()
591 s = Database._status_string(self._db)
593 return s.decode('utf-8', 'ignore')
597 return "'Notmuch DB " + self.get_path() + "'"
599 def _get_user_default_db(self):
600 """ Reads a user's notmuch config and returns his db location
602 Throws a NotmuchError if it cannot find it"""
603 config = SafeConfigParser()
604 conf_f = os.getenv('NOTMUCH_CONFIG',
605 os.path.expanduser('~/.notmuch-config'))
606 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
607 if not config.has_option('database', 'path'):
608 raise NotmuchError(message="No DB path specified"
609 " and no user default found")
610 return config.get('database', 'path')