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 (
30 ReadOnlyDatabaseError,
38 from notmuch.message import Message
39 from notmuch.tag import Tags
40 from .query import Query
41 from .directory import Directory
43 class Database(object):
44 """The :class:`Database` is the highest-level object that notmuch
45 provides. It references a notmuch database, and can be opened in
46 read-only or read-write mode. A :class:`Query` can be derived from
47 or be applied to a specific database to find messages. Also adding
48 and removing messages to the database happens via this
49 object. Modifications to the database are not atmic by default (see
50 :meth:`begin_atomic`) and once a database has been modified, all
51 other database objects pointing to the same data-base will throw an
52 :exc:`XapianError` as the underlying database has been
53 modified. Close and reopen the database to continue working with it.
55 :class:`Database` objects implement the context manager protocol
56 so you can use the :keyword:`with` statement to ensure that the
57 database is properly closed.
61 Any function in this class can and will throw an
62 :exc:`NotInitializedError` if the database was not intitialized
67 Do remember that as soon as we tear down (e.g. via `del db`) this
68 object, all underlying derived objects such as queries, threads,
69 messages, tags etc will be freed by the underlying library as well.
70 Accessing these objects will lead to segfaults and other unexpected
71 behavior. See above for more details.
74 """Class attribute to cache user's default database"""
76 MODE = Enum(['READ_ONLY', 'READ_WRITE'])
77 """Constants: Mode in which to open the database"""
79 """notmuch_database_get_directory"""
80 _get_directory = nmlib.notmuch_database_get_directory
81 _get_directory.argtypes = [NotmuchDatabaseP, c_char_p]
82 _get_directory.restype = NotmuchDirectoryP
84 """notmuch_database_get_path"""
85 _get_path = nmlib.notmuch_database_get_path
86 _get_path.argtypes = [NotmuchDatabaseP]
87 _get_path.restype = c_char_p
89 """notmuch_database_get_version"""
90 _get_version = nmlib.notmuch_database_get_version
91 _get_version.argtypes = [NotmuchDatabaseP]
92 _get_version.restype = c_uint
94 """notmuch_database_open"""
95 _open = nmlib.notmuch_database_open
96 _open.argtypes = [c_char_p, c_uint]
97 _open.restype = NotmuchDatabaseP
99 """notmuch_database_upgrade"""
100 _upgrade = nmlib.notmuch_database_upgrade
101 _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
102 _upgrade.restype = c_uint
104 """ notmuch_database_find_message"""
105 _find_message = nmlib.notmuch_database_find_message
106 _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
107 POINTER(NotmuchMessageP)]
108 _find_message.restype = c_uint
110 """notmuch_database_find_message_by_filename"""
111 _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
112 _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
113 POINTER(NotmuchMessageP)]
114 _find_message_by_filename.restype = c_uint
116 """notmuch_database_get_all_tags"""
117 _get_all_tags = nmlib.notmuch_database_get_all_tags
118 _get_all_tags.argtypes = [NotmuchDatabaseP]
119 _get_all_tags.restype = NotmuchTagsP
121 """notmuch_database_create"""
122 _create = nmlib.notmuch_database_create
123 _create.argtypes = [c_char_p]
124 _create.restype = NotmuchDatabaseP
126 def __init__(self, path = None, create = False,
127 mode = MODE.READ_ONLY):
128 """If *path* is `None`, we will try to read a users notmuch
129 configuration and use his configured database. The location of the
130 configuration file can be specified through the environment variable
131 *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
133 If *create* is `True`, the database will always be created in
134 :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
136 :param path: Directory to open/create the database in (see
137 above for behavior if `None`)
138 :type path: `str` or `None`
139 :param create: Pass `False` to open an existing, `True` to create a new
142 :param mode: Mode to open a database in. Is always
143 :attr:`MODE`.READ_WRITE when creating a new one.
144 :type mode: :attr:`MODE`
145 :raises: :exc:`NotmuchError` or derived exception in case of
151 # no path specified. use a user's default database
152 if Database._std_db_path is None:
153 #the following line throws a NotmuchError if it fails
154 Database._std_db_path = self._get_user_default_db()
155 path = Database._std_db_path
158 self.open(path, mode)
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).
185 if self._db is not None:
186 raise NotmuchError(message="Cannot create db, this Database() "
187 "already has an open one.")
189 res = Database._create(_str(path), Database.MODE.READ_WRITE)
193 message="Could not create the specified database")
196 def open(self, path, mode=0):
197 """Opens an existing database
199 This function is used by __init__() and usually does not need
200 to be called directly. It wraps the underlying
201 *notmuch_database_open* function.
203 :param status: Open the database in read-only or read-write mode
204 :type status: :attr:`MODE`
205 :raises: Raises :exc:`NotmuchError` in case of any failure
206 (possibly after printing an error message on stderr).
208 res = Database._open(_str(path), mode)
211 raise NotmuchError(message="Could not open the specified database")
214 _close = nmlib.notmuch_database_close
215 _close.argtypes = [NotmuchDatabaseP]
216 _close.restype = None
219 """Close and free the notmuch database if needed"""
220 if self._db is not None:
221 self._close(self._db)
226 Implements the context manager protocol.
230 def __exit__(self, exc_type, exc_value, traceback):
232 Implements the context manager protocol.
237 """Returns the file path of an open database"""
238 self._assert_db_is_initialized()
239 return Database._get_path(self._db).decode('utf-8')
241 def get_version(self):
242 """Returns the database format version
244 :returns: The database version as positive integer
246 self._assert_db_is_initialized()
247 return Database._get_version(self._db)
249 _needs_upgrade = nmlib.notmuch_database_needs_upgrade
250 _needs_upgrade.argtypes = [NotmuchDatabaseP]
251 _needs_upgrade.restype = bool
253 def needs_upgrade(self):
254 """Does this database need to be upgraded before writing to it?
256 If this function returns `True` then no functions that modify the
257 database (:meth:`add_message`,
258 :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
259 etc.) will work unless :meth:`upgrade` is called successfully first.
261 :returns: `True` or `False`
263 self._assert_db_is_initialized()
264 return self._needs_upgrade(self._db)
267 """Upgrades the current database
269 After opening a database in read-write mode, the client should
270 check if an upgrade is needed (notmuch_database_needs_upgrade) and
271 if so, upgrade with this function before making any modifications.
273 NOT IMPLEMENTED: The optional progress_notify callback can be
274 used by the caller to provide progress indication to the
275 user. If non-NULL it will be called periodically with
276 'progress' as a floating-point value in the range of [0.0..1.0]
277 indicating the progress made so far in the upgrade process.
279 :TODO: catch exceptions, document return values and etc...
281 self._assert_db_is_initialized()
282 status = Database._upgrade(self._db, None, None)
283 #TODO: catch exceptions, document return values and etc
286 _begin_atomic = nmlib.notmuch_database_begin_atomic
287 _begin_atomic.argtypes = [NotmuchDatabaseP]
288 _begin_atomic.restype = c_uint
290 def begin_atomic(self):
291 """Begin an atomic database operation
293 Any modifications performed between a successful
294 :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
295 the database atomically. Note that, unlike a typical database
296 transaction, this only ensures atomicity, not durability;
297 neither begin nor end necessarily flush modifications to disk.
299 :returns: :attr:`STATUS`.SUCCESS or raises
300 :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
301 Xapian exception occurred; atomic section not entered.
303 *Added in notmuch 0.9*"""
304 self._assert_db_is_initialized()
305 status = self._begin_atomic(self._db)
306 if status != STATUS.SUCCESS:
307 raise NotmuchError(status)
310 _end_atomic = nmlib.notmuch_database_end_atomic
311 _end_atomic.argtypes = [NotmuchDatabaseP]
312 _end_atomic.restype = c_uint
314 def end_atomic(self):
315 """Indicate the end of an atomic database operation
317 See :meth:`begin_atomic` for details.
319 :returns: :attr:`STATUS`.SUCCESS or raises
323 :attr:`STATUS`.XAPIAN_EXCEPTION
324 A Xapian exception occurred; atomic section not
326 :attr:`STATUS`.UNBALANCED_ATOMIC:
327 end_atomic has been called more times than begin_atomic.
329 *Added in notmuch 0.9*"""
330 self._assert_db_is_initialized()
331 status = self._end_atomic(self._db)
332 if status != STATUS.SUCCESS:
333 raise NotmuchError(status)
336 def get_directory(self, path):
337 """Returns a :class:`Directory` of path,
338 (creating it if it does not exist(?))
340 :param path: An unicode string containing the path relative to the path
341 of database (see :meth:`get_path`), or else should be an absolute
342 path with initial components that match the path of 'database'.
343 :returns: :class:`Directory` or raises an exception.
344 :raises: :exc:`FileError` if path is not relative database or absolute
345 with initial components same as database.
346 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
347 opened in read-write mode
349 self._assert_db_is_initialized()
351 # work around libnotmuch calling exit(3), see
352 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
353 # TODO: remove once this issue is resolved
354 if self.mode != Database.MODE.READ_WRITE:
355 raise ReadOnlyDatabaseError('The database has to be opened in '
356 'read-write mode for get_directory')
358 # sanity checking if path is valid, and make path absolute
359 if path and path[0] == os.sep:
360 # we got an absolute path
361 if not path.startswith(self.get_path()):
362 # but its initial components are not equal to the db path
363 raise FileError('Database().get_directory() called '
364 'with a wrong absolute path')
367 #we got a relative path, make it absolute
368 abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
370 dir_p = Database._get_directory(self._db, _str(path))
372 # return the Directory, init it with the absolute path
373 return Directory(abs_dirpath, dir_p, self)
375 _add_message = nmlib.notmuch_database_add_message
376 _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
377 POINTER(NotmuchMessageP)]
378 _add_message.restype = c_uint
380 def add_message(self, filename, sync_maildir_flags=False):
381 """Adds a new message to the database
383 :param filename: should be a path relative to the path of the
384 open database (see :meth:`get_path`), or else should be an
385 absolute filename with initial components that match the
386 path of the database.
388 The file should be a single mail message (not a
389 multi-message mbox) that is expected to remain at its
390 current location, since the notmuch database will reference
391 the filename, and will not copy the entire contents of the
394 :param sync_maildir_flags: If the message contains Maildir
395 flags, we will -depending on the notmuch configuration- sync
396 those tags to initial notmuch tags, if set to `True`. It is
397 `False` by default to remain consistent with the libnotmuch
398 API. You might want to look into the underlying method
399 :meth:`Message.maildir_flags_to_tags`.
401 :returns: On success, we return
403 1) a :class:`Message` object that can be used for things
404 such as adding tags to the just-added message.
405 2) one of the following :attr:`STATUS` values:
407 :attr:`STATUS`.SUCCESS
408 Message successfully added to database.
409 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
410 Message has the same message ID as another message already
411 in the database. The new filename was successfully added
412 to the list of the filenames for the existing message.
414 :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
416 :raises: Raises a :exc:`NotmuchError` with the following meaning.
417 If such an exception occurs, nothing was added to the database.
419 :attr:`STATUS`.FILE_ERROR
420 An error occurred trying to open the file, (such as
421 permission denied, or file not found, etc.).
422 :attr:`STATUS`.FILE_NOT_EMAIL
423 The contents of filename don't look like an email
425 :attr:`STATUS`.READ_ONLY_DATABASE
426 Database was opened in read-only mode so no message can
429 self._assert_db_is_initialized()
430 msg_p = NotmuchMessageP()
431 status = self._add_message(self._db, _str(filename), byref(msg_p))
433 if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
434 raise NotmuchError(status)
436 #construct Message() and return
437 msg = Message(msg_p, self)
438 #automatic sync initial tags from Maildir flags
439 if sync_maildir_flags:
440 msg.maildir_flags_to_tags()
443 _remove_message = nmlib.notmuch_database_remove_message
444 _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
445 _remove_message.restype = c_uint
447 def remove_message(self, filename):
448 """Removes a message (filename) from the given notmuch database
450 Note that only this particular filename association is removed from
451 the database. If the same message (as determined by the message ID)
452 is still available via other filenames, then the message will
453 persist in the database for those filenames. When the last filename
454 is removed for a particular message, the database content for that
455 message will be entirely removed.
457 :returns: A :attr:`STATUS` value with the following meaning:
459 :attr:`STATUS`.SUCCESS
460 The last filename was removed and the message was removed
462 :attr:`STATUS`.DUPLICATE_MESSAGE_ID
463 This filename was removed but the message persists in the
464 database with at least one other filename.
466 :raises: Raises a :exc:`NotmuchError` with the following meaning.
467 If such an exception occurs, nothing was removed from the
470 :attr:`STATUS`.READ_ONLY_DATABASE
471 Database was opened in read-only mode so no message can be
474 self._assert_db_is_initialized()
475 return self._remove_message(self._db, _str(filename))
477 def find_message(self, msgid):
478 """Returns a :class:`Message` as identified by its message ID
480 Wraps the underlying *notmuch_database_find_message* function.
482 :param msgid: The message ID
483 :type msgid: unicode or str
484 :returns: :class:`Message` or `None` if no message is found.
486 :exc:`OutOfMemoryError`
487 If an Out-of-memory occured while constructing the message.
489 In case of a Xapian Exception. These exceptions
490 include "Database modified" situations, e.g. when the
491 notmuch database has been modified by another program
492 in the meantime. In this case, you should close and
493 reopen the database and retry.
494 :exc:`NotInitializedError` if
495 the database was not intitialized.
497 self._assert_db_is_initialized()
498 msg_p = NotmuchMessageP()
499 status = Database._find_message(self._db, _str(msgid), byref(msg_p))
500 if status != STATUS.SUCCESS:
501 raise NotmuchError(status)
502 return msg_p and Message(msg_p, self) or None
504 def find_message_by_filename(self, filename):
505 """Find a message with the given filename
507 :returns: If the database contains a message with the given
508 filename, then a class:`Message:` is returned. This
509 function returns None if no message is found with the given
512 :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while
513 constructing the message.
514 :raises: :exc:`XapianError` in case of a Xapian Exception.
515 These exceptions include "Database modified"
516 situations, e.g. when the notmuch database has been
517 modified by another program in the meantime. In this
518 case, you should close and reopen the database and
520 :raises: :exc:`NotInitializedError` if the database was not
522 :raises: :exc:`ReadOnlyDatabaseError` if the database has not been
523 opened in read-write mode
525 *Added in notmuch 0.9*"""
526 self._assert_db_is_initialized()
528 # work around libnotmuch calling exit(3), see
529 # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de
530 # TODO: remove once this issue is resolved
531 if self.mode != Database.MODE.READ_WRITE:
532 raise ReadOnlyDatabaseError('The database has to be opened in '
533 'read-write mode for get_directory')
535 msg_p = NotmuchMessageP()
536 status = Database._find_message_by_filename(self._db, _str(filename),
538 if status != STATUS.SUCCESS:
539 raise NotmuchError(status)
540 return msg_p and Message(msg_p, self) or None
542 def get_all_tags(self):
543 """Returns :class:`Tags` with a list of all tags found in the database
545 :returns: :class:`Tags`
546 :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
549 self._assert_db_is_initialized()
550 tags_p = Database._get_all_tags(self._db)
552 raise NullPointerError()
553 return Tags(tags_p, self)
555 def create_query(self, querystring):
556 """Returns a :class:`Query` derived from this database
558 This is a shorthand method for doing::
561 # Automatically frees the Database() when 'q' is deleted
563 q = Database(dbpath).create_query('from:"Biene Maja"')
565 # long version, which is functionally equivalent but will keep the
566 # Database in the 'db' variable around after we delete 'q':
568 db = Database(dbpath)
569 q = Query(db,'from:"Biene Maja"')
571 This function is a python extension and not in the underlying C API.
573 return Query(self, querystring)
576 return "'Notmuch DB " + self.get_path() + "'"
578 def _get_user_default_db(self):
579 """ Reads a user's notmuch config and returns his db location
581 Throws a NotmuchError if it cannot find it"""
584 from configparser import SafeConfigParser
587 from ConfigParser import SafeConfigParser
589 config = SafeConfigParser()
590 conf_f = os.getenv('NOTMUCH_CONFIG',
591 os.path.expanduser('~/.notmuch-config'))
592 config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
593 if not config.has_option('database', 'path'):
594 raise NotmuchError(message="No DB path specified"
595 " and no user default found")
596 return config.get('database', 'path')
600 """Property returning a pointer to `notmuch_database_t` or `None`
602 This should normally not be needed by a user (and is not yet
603 guaranteed to remain stable in future versions).